2026-08-15 09:10:10 +08:00
package repository
import (
"database/sql"
2026-08-15 09:12:23 +08:00
"errors"
2026-08-15 10:49:28 +08:00
"strings"
2026-08-15 09:10:10 +08:00
"testing"
2026-08-15 09:12:23 +08:00
"github.com/go-sql-driver/mysql"
2026-08-15 09:10:10 +08:00
_ "modernc.org/sqlite"
2026-08-15 10:49:28 +08:00
"cmautobuy/admin/model"
2026-08-15 09:10:10 +08:00
)
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
2026-08-15 17:08:17 +08:00
(1,'applying','old'),(2,'ready','old'),(3,'queued','old')` ); err != nil {
2026-08-15 09:10:10 +08:00
t . Fatal ( err )
}
count , err := InterruptApplyingInnerCodes ( db , "2026-08-15T01:00:00Z" )
2026-08-15 17:08:17 +08:00
if err != nil || count != 2 {
2026-08-15 09:10:10 +08:00
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 )
}
2026-08-15 17:08:17 +08:00
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 )
}
2026-08-15 09:10:10 +08:00
}
2026-08-15 09:12:23 +08:00
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 )
}
}
2026-08-15 10:49:28 +08:00
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 )
}
2026-08-15 17:08:17 +08:00
statuses := [] string { "pending" , "ready" , "queued" , "applying" , "updated" , "already_filled" , "skipped" , "failed" , "needs_check" }
2026-08-15 10:49:28 +08:00
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 )
}
}
2026-08-15 17:08:17 +08:00
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 ++
}
}
2026-08-15 10:49:28 +08:00
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 )
}
}
2026-08-15 17:08:17 +08:00
for _ , status := range [] string { "queued" , "applying" , "updated" , "already_filled" , "needs_check" } {
2026-08-15 10:49:28 +08:00
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 )
}
}