package common import ( "context" "errors" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) func TestBatchMainRequeuePublishesPendingBatch(t *testing.T) { ctx := setupBatchMainRequeueDB(t) insertRequeueBatch(t, ctx, "PENDING-1", consts.BatchTypeInbound, consts.BatchAsyncStatusPending, consts.BatchRunResultPending, `{}`) var published []batchExecuteMessage restore := replaceBatchRequeuePublisher(func(_ context.Context, queue string, data any) error { if queue != consts.QueueBatchExecute { t.Fatalf("queue = %s, want %s", queue, consts.QueueBatchExecute) } message, ok := data.(batchExecuteMessage) if !ok { t.Fatalf("message type = %T, want batchExecuteMessage", data) } published = append(published, message) return nil }) defer restore() res, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "PENDING-1", Reason: "message lost"}) if err != nil { t.Fatalf("Requeue() error = %v", err) } if res == nil || !res.Requeued || res.BatchNo != "PENDING-1" || res.BatchType != consts.BatchTypeInbound { t.Fatalf("unexpected response: %+v", res) } if len(published) != 1 || published[0].BatchNo != "PENDING-1" || published[0].BatchType != consts.BatchTypeInbound { t.Fatalf("unexpected published messages: %+v", published) } } func TestBatchMainRequeuePublishesDueRestartWaitingResume(t *testing.T) { ctx := setupBatchMainRequeueDB(t) resumeAt := gtime.Now().Add(-time.Minute).String() insertRequeueBatch(t, ctx, "RST-DUE", consts.BatchTypeRestart, consts.BatchAsyncStatusProcessing, consts.BatchRunResultRunning, `{"restart":{"phase":"waiting_resume","resumeAt":"`+resumeAt+`"}}`) var published []batchExecuteMessage restore := replaceBatchRequeuePublisher(func(_ context.Context, _ string, data any) error { published = append(published, data.(batchExecuteMessage)) return nil }) defer restore() res, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "RST-DUE", Reason: "delayed message lost"}) if err != nil { t.Fatalf("Requeue() error = %v", err) } if res == nil || res.BatchNo != "RST-DUE" || res.BatchType != consts.BatchTypeRestart { t.Fatalf("unexpected response: %+v", res) } if len(published) != 1 || published[0].BatchNo != "RST-DUE" || published[0].BatchType != consts.BatchTypeRestart { t.Fatalf("unexpected published messages: %+v", published) } } func TestBatchMainRequeueRejectsUnsafeStatuses(t *testing.T) { ctx := setupBatchMainRequeueDB(t) future := gtime.Now().Add(time.Hour).String() cases := []struct { name string batchNo string batchType int status int runResult int info string }{ {name: "failed", batchNo: "FAILED-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusFailed, runResult: consts.BatchRunResultFailed, info: `{}`}, {name: "success", batchNo: "SUCCESS-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusSuccess, runResult: consts.BatchRunResultSuccess, info: `{}`}, {name: "partial", batchNo: "PARTIAL-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusPartialSuccess, runResult: consts.BatchRunResultPartialSuccess, info: `{}`}, {name: "review", batchNo: "REVIEW-1", batchType: consts.BatchTypeDataStart, status: consts.BatchAsyncStatusReview, runResult: consts.BatchRunResultReview, info: `{}`}, {name: "normal processing", batchNo: "PROCESSING-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusProcessing, runResult: consts.BatchRunResultRunning, info: `{}`}, {name: "restart future resume", batchNo: "RST-FUTURE", batchType: consts.BatchTypeRestart, status: consts.BatchAsyncStatusProcessing, runResult: consts.BatchRunResultRunning, info: `{"restart":{"phase":"waiting_resume","resumeAt":"` + future + `"}}`}, } for _, tc := range cases { insertRequeueBatch(t, ctx, tc.batchNo, tc.batchType, tc.status, tc.runResult, tc.info) } var published int restore := replaceBatchRequeuePublisher(func(context.Context, string, any) error { published++ return nil }) defer restore() for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: tc.batchNo, Reason: "operator retry"}) if err == nil { t.Fatal("Requeue() error = nil, want error") } }) } if published != 0 { t.Fatalf("published messages = %d, want 0", published) } } func TestBatchMainRequeueHandlesMissingBatchAndPublishFailure(t *testing.T) { ctx := setupBatchMainRequeueDB(t) insertRequeueBatch(t, ctx, "PENDING-FAIL-PUBLISH", consts.BatchTypeInbound, consts.BatchAsyncStatusPending, consts.BatchRunResultPending, `{}`) restore := replaceBatchRequeuePublisher(func(context.Context, string, any) error { return errors.New("rabbitmq unavailable") }) defer restore() if _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "PENDING-FAIL-PUBLISH", Reason: "message lost"}); err == nil { t.Fatal("Requeue() publish failure error = nil, want error") } if _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "MISSING", Reason: "message lost"}); err == nil { t.Fatal("Requeue() missing batch error = nil, want error") } } func setupBatchMainRequeueDB(t *testing.T) context.Context { t.Helper() ctx := context.Background() if err := gdb.SetConfig(gdb.Config{ "default": gdb.ConfigGroup{{ Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1, }}, }); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = g.DB().Close(ctx) }) // GoFrame caches table fields by group and table, across SQLite instances. if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil { t.Fatal(err) } if _, err := g.DB().Exec(ctx, `CREATE TABLE batch_main ( source_channel TEXT NOT NULL DEFAULT 'legacy', execution_mode INTEGER NOT NULL DEFAULT 0, id INTEGER PRIMARY KEY AUTOINCREMENT, batch_no TEXT NOT NULL, batch_type INTEGER NOT NULL, batch_async_status INTEGER NOT NULL, run_result INTEGER NOT NULL, batch_info TEXT, tenant_id TEXT NOT NULL DEFAULT '', dept_id INTEGER NOT NULL DEFAULT 0, created_dept INTEGER NOT NULL DEFAULT 0, created_by INTEGER NOT NULL DEFAULT 0 )`); err != nil { t.Fatal(err) } return ctx } func insertRequeueBatch(t *testing.T, ctx context.Context, batchNo string, batchType, status, runResult int, batchInfo string) { t.Helper() if _, err := g.DB().Exec(ctx, `INSERT INTO batch_main(batch_no, batch_type, batch_async_status, run_result, batch_info, tenant_id) VALUES(?, ?, ?, ?, ?, '')`, batchNo, batchType, status, runResult, batchInfo, ); err != nil { t.Fatal(err) } } func replaceBatchRequeuePublisher(fn func(context.Context, string, any) error) func() { original := publishBatchExecuteMessage publishBatchExecuteMessage = fn return func() { publishBatchExecuteMessage = original } }