package repository import ( "database/sql" "errors" "strings" "testing" "github.com/go-sql-driver/mysql" _ "modernc.org/sqlite" "cmautobuy/admin/model" ) func TestInterruptApplyingInnerCodes_只收敛回写中记录(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_interrupt?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,result_message TEXT,updated_at TEXT NOT NULL )`); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES (1,'applying','old'),(2,'ready','old'),(3,'queued','old')`); err != nil { t.Fatal(err) } count, err := InterruptApplyingInnerCodes(db, "2026-08-15T01:00:00Z") if err != nil || count != 2 { t.Fatalf("count=%d err=%v", count, err) } var status, message, updatedAt string if err := db.QueryRow(`SELECT status,result_message,updated_at FROM syb_inner_code_records WHERE id=1`). Scan(&status, &message, &updatedAt); err != nil { t.Fatal(err) } if status != "needs_check" || message == "" || updatedAt != "2026-08-15T01:00:00Z" { t.Fatalf("status=%s message=%q updated=%s", status, message, updatedAt) } if err := db.QueryRow(`SELECT status FROM syb_inner_code_records WHERE id=2`).Scan(&status); err != nil || status != "ready" { t.Fatalf("ready 记录不应变化 status=%s err=%v", status, err) } if err := db.QueryRow(`SELECT status,result_message FROM syb_inner_code_records WHERE id=3`).Scan(&status, &message); err != nil || status != "ready" || message == "" { t.Fatalf("queued 记录应恢复可回写 status=%s message=%q err=%v", status, message, err) } } func TestInnerCodeImportWriteError_唯一冲突不泄漏索引细节(t *testing.T) { err := innerCodeImportWriteError("新增失败", &mysql.MySQLError{Number: 1062, Message: "Duplicate entry for uq-secret"}) if !errors.Is(err, ErrInnerCodeUniqueConflict) { t.Fatalf("应识别 MySQL 1062: %v", err) } if err.Error() != "新增失败: "+ErrInnerCodeUniqueConflict.Error() { t.Fatalf("不应回显索引细节: %v", err) } } func TestSoftDeleteInnerCodeRecords_所有状态只写删除审计(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_delete?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,deleted_at TEXT,deleted_by_user_id TEXT,updated_at TEXT NOT NULL )`); err != nil { t.Fatal(err) } statuses := []string{"pending", "ready", "queued", "applying", "updated", "already_filled", "skipped", "failed", "needs_check"} for index, status := range statuses { if _, err := db.Exec(`INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES(?,?,?)`, index+1, status, "old"); err != nil { t.Fatal(err) } } ids := make([]int64, len(statuses)) for index := range ids { ids[index] = int64(index + 1) } count, err := SoftDeleteInnerCodeRecords(db, ids, "user-1", "2026-08-15T02:00:00Z") if err != nil || count != len(statuses) { t.Fatalf("count=%d err=%v", count, err) } rows, err := db.Query(`SELECT status,deleted_at,deleted_by_user_id,updated_at FROM syb_inner_code_records ORDER BY id`) if err != nil { t.Fatal(err) } defer rows.Close() index := 0 for rows.Next() { var status, deletedAt, actor, updatedAt string if err := rows.Scan(&status, &deletedAt, &actor, &updatedAt); err != nil { t.Fatal(err) } if status != statuses[index] || deletedAt != "2026-08-15T02:00:00Z" || actor != "user-1" || updatedAt != deletedAt { t.Fatalf("第 %d 行状态或审计变化错误: %s %s %s %s", index+1, status, deletedAt, actor, updatedAt) } index++ } } func TestSoftDeleteInnerCodeRecords_有失效ID时整批回滚(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_delete_conflict?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,deleted_at TEXT,deleted_by_user_id TEXT,updated_at TEXT NOT NULL ); INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES(1,'pending','old')`); err != nil { t.Fatal(err) } if _, err := SoftDeleteInnerCodeRecords(db, []int64{1, 99}, "user-1", "2026-08-15T02:00:00Z"); !errors.Is(err, ErrInnerCodeDeleteConflict) { t.Fatalf("期望整批冲突,实际 %v", err) } var deletedAt sql.NullString if err := db.QueryRow(`SELECT deleted_at FROM syb_inner_code_records WHERE id=1`).Scan(&deletedAt); err != nil { t.Fatal(err) } if deletedAt.Valid { t.Fatalf("整批回滚后不应保留删除标记: %+v", deletedAt) } } func TestQueueInnerCodeApplyBatch_状态冲突时整批回滚(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_queue_conflict?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT,apply_queued_at TEXT, applied_by_user_id TEXT,result_message TEXT,apply_started_at TEXT,updated_at TEXT, deleted_at TEXT ); INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES (1,'ready','old'),(2,'pending','old')`); err != nil { t.Fatal(err) } if _, err := QueueInnerCodeApplyBatch(db, []int64{1, 2}, "ICB-1", "user-1", "2026-08-15T05:00:00Z"); !errors.Is(err, ErrInnerCodeApplyConflict) { t.Fatalf("期望整批排队冲突,实际 %v", err) } var status string var batchID sql.NullString if err := db.QueryRow(`SELECT status,apply_batch_id FROM syb_inner_code_records WHERE id=1`).Scan(&status, &batchID); err != nil { t.Fatal(err) } if status != "ready" || batchID.Valid { t.Fatalf("冲突后不应部分入队 status=%s batch=%+v", status, batchID) } } func TestInnerCodeApplyBatchProgress_聚合终态与恢复记录(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_batch_progress?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT ); INSERT INTO syb_inner_code_records(id,status,apply_batch_id) VALUES (1,'queued','ICB-1'),(2,'applying','ICB-1'),(3,'updated','ICB-1'), (4,'failed','ICB-1'),(5,'needs_check','ICB-1'),(6,'ready','ICB-1')`); err != nil { t.Fatal(err) } progress, err := GetInnerCodeApplyBatchProgress(db, "ICB-1") if err != nil { t.Fatal(err) } if progress == nil || progress.Total != 6 || progress.Queued != 1 || progress.Applying != 1 || progress.Updated != 1 || progress.Failed != 1 || progress.NeedsCheck != 1 || progress.Ready != 1 || progress.Processed() != 3 { t.Fatalf("progress=%+v", progress) } } func TestInterruptInnerCodeApplyBatch_区分未知结果和未开始记录(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_batch_interrupt?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT,result_message TEXT,updated_at TEXT ); INSERT INTO syb_inner_code_records(id,status,apply_batch_id,updated_at) VALUES (1,'applying','ICB-1','old'),(2,'queued','ICB-1','old'),(3,'queued','ICB-2','old')`); err != nil { t.Fatal(err) } needsCheck, released, err := InterruptInnerCodeApplyBatch(db, "ICB-1", "2026-08-15T06:00:00Z") if err != nil || needsCheck != 1 || released != 1 { t.Fatalf("needs_check=%d released=%d err=%v", needsCheck, released, err) } rows, err := db.Query(`SELECT id,status,result_message,updated_at FROM syb_inner_code_records ORDER BY id`) if err != nil { t.Fatal(err) } defer rows.Close() wants := []string{"needs_check", "ready", "queued"} index := 0 for rows.Next() { var id int var status, message, updatedAt string var nullableMessage sql.NullString if err := rows.Scan(&id, &status, &nullableMessage, &updatedAt); err != nil { t.Fatal(err) } message = nullableMessage.String if status != wants[index] || (id < 3 && (message == "" || updatedAt != "2026-08-15T06:00:00Z")) { t.Fatalf("id=%d status=%s message=%q updated=%s", id, status, message, updatedAt) } index++ } } func TestFinishInnerCodeApply_软删除后仍保存后台结果(t *testing.T) { db, err := sql.Open("sqlite", "file:inner_code_finish_deleted?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,result_message TEXT,remote_inner_code TEXT, applied_at TEXT,updated_at TEXT NOT NULL,deleted_at TEXT ); INSERT INTO syb_inner_code_records(id,status,updated_at,deleted_at) VALUES(1,'applying','old','2026-08-15T02:00:00Z')`); err != nil { t.Fatal(err) } if err := FinishInnerCodeApply(db, 1, model.InnerCodeUpdated, "远端核验成功", "DK-1", "2026-08-15T02:01:00Z"); err != nil { t.Fatal(err) } var status, message, remoteCode, appliedAt, deletedAt string if err := db.QueryRow(`SELECT status,result_message,remote_inner_code,applied_at,deleted_at FROM syb_inner_code_records WHERE id=1`).Scan(&status, &message, &remoteCode, &appliedAt, &deletedAt); err != nil { t.Fatal(err) } if status != "updated" || message != "远端核验成功" || remoteCode != "DK-1" || appliedAt != "2026-08-15T02:01:00Z" || deletedAt != "2026-08-15T02:00:00Z" { t.Fatalf("隐藏记录后台收尾错误: %s %s %s %s %s", status, message, remoteCode, appliedAt, deletedAt) } } func TestInnerCodeStatusPreservesImportResult(t *testing.T) { for _, status := range []string{"pending", "ready", "skipped", "failed"} { if innerCodeStatusPreservesImportResult(model.InnerCodeStatus(status)) { t.Errorf("%s 不应保留旧规划", status) } } for _, status := range []string{"queued", "applying", "updated", "already_filled", "needs_check"} { if !innerCodeStatusPreservesImportResult(model.InnerCodeStatus(status)) { t.Errorf("%s 应保留远端结果和审计", status) } } } func TestInnerCodeFilterClause_正常列表固定隐藏软删除记录(t *testing.T) { where, args := innerCodeFilterClause(InnerCodeListFilter{BusinessDate: "2026-08-15", Status: "ready", Keyword: "ORDER"}) if !strings.Contains(where, "deleted_at IS NULL") || len(args) != 6 { t.Fatalf("列表软删除过滤不完整: where=%q args=%v", where, args) } }