Files
iotlcm/server/internal/logic/app/app_card_allocate_queue_test.go
T

262 lines
12 KiB
Go

package app
import (
"context"
"errors"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"reflect"
"regexp"
"strings"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/allocaterequest"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
)
type allocateSequenceAdapter struct {
gredis.Adapter
sequence int
}
func (adapter *allocateSequenceAdapter) Do(ctx context.Context, command string, args ...any) (*gvar.Var, error) {
if command != "EVAL" {
return nil, errors.New("unexpected Redis command")
}
adapter.sequence++
return gvar.New(adapter.sequence), nil
}
func TestAppAllocateQueuePersistsWorkerTaskWithoutChangingOwnership(t *testing.T) {
ctx := context.Background()
gredis.SetConfig(&gredis.Config{Address: "127.0.0.1:1"})
redis := g.Redis()
original := redis.GetAdapter()
redis.SetAdapter(&allocateSequenceAdapter{})
t.Cleanup(func() { redis.SetAdapter(original) })
previous := gdb.GetConfig("default")
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}); err != nil {
t.Fatal(err)
}
database := g.DB()
t.Cleanup(func() {
if err := database.GetCore().ClearCacheAll(ctx); err != nil {
t.Error(err)
}
if err := database.Close(ctx); err != nil {
t.Error(err)
}
if err := gdb.SetConfigGroup("default", previous); err != nil {
t.Error(err)
}
})
if err := database.GetCore().ClearCacheAll(ctx); err != nil {
t.Fatal(err)
}
for name, value := range map[string]any{"batch_main": entity.BatchMain{}, "batch_detail": entity.BatchDetail{}, "t_sim_card_info": entity.TSimCardInfo{}} {
typ := reflect.TypeOf(value)
columns := make([]string, 0, typ.NumField())
for index := 0; index < typ.NumField(); index++ {
field := typ.Field(index)
column := field.Tag.Get("orm")
sqlType := "TEXT"
if field.Type.Kind() >= reflect.Int && field.Type.Kind() <= reflect.Uint64 {
sqlType = "INTEGER"
}
if column == "id" {
sqlType = "INTEGER PRIMARY KEY"
}
if column == "batch_no" && name == "batch_main" {
sqlType += " UNIQUE"
}
columns = append(columns, column+" "+sqlType)
}
if _, err := g.DB().Exec(ctx, "CREATE TABLE "+name+" ("+strings.Join(columns, ",")+")"); err != nil {
t.Fatal(err)
}
}
if _, err := g.DB().Exec(ctx, "CREATE TABLE sys_dept(dept_id INTEGER,tenant_id TEXT,dept_key TEXT,dept_name TEXT,deleted_at TEXT)"); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, "INSERT INTO sys_dept VALUES(1,'A','source','划拨单位',NULL),(2,'A','target','接收单位',NULL)"); err != nil {
t.Fatal(err)
}
user := &entity.SysUser{UserId: 7, DeptId: 1, TenantId: "A"}
prepare := &entity.BatchMain{BatchInfo: gjson.New(map[string]any{"requestId": "request", "requestHash": "hash"}), Id: 1, BatchNo: "ALC20261012000001", ExecutionMode: 2}
if _, err := dao.BatchMain.Ctx(ctx).Data(do.BatchMain{Id: 1, BatchNo: prepare.BatchNo, BatchType: 4, TenantId: "A", CreatedBy: 7, StartTime: gtime.Now(), EndTime: gtime.Now(), TotalCount: 1, ResultOssId: 99}).Insert(); err != nil {
t.Fatal(err)
}
if _, err := dao.BatchDetail.Ctx(ctx).Data(do.BatchDetail{BatchNo: prepare.BatchNo, BatchType: 4, ItemId: "CARD", TenantId: "A", CreatedBy: 7}).Insert(); err != nil {
t.Fatal(err)
}
if _, err := dao.TSimCardInfo.Ctx(ctx).Data(do.TSimCardInfo{Id: 1, Iccid: "CARD", DeptId: 1, TenantId: "A", CreatedDept: 1}).Insert(); err != nil {
t.Fatal(err)
}
batch := &appAllocateBatchContext{Target: &entity.SysDept{DeptId: 2, DeptName: "目标客户"}, Cards: []*appAllocateCardContext{{Card: &appCardQueryRow{TSimCardInfo: entity.TSimCardInfo{Id: 1, Iccid: "CARD", DeptId: 1}}}}}
if err := dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
return NewAppCard().queueAppAllocateBatch(ctx, tx, user, prepare, batch, "")
}); err != nil {
t.Fatal(err)
}
var parent *entity.BatchMain
if err := dao.BatchMain.Ctx(ctx).Where("batch_no", prepare.BatchNo).Scan(&parent); err != nil {
t.Fatal(err)
}
if parent.BatchType != consts.BatchTypeAllocate || parent.SourceBatchNo != "" || parent.BatchAsyncStatus != consts.BatchAsyncStatusPending || parent.ExecutionMode != 2 || parent.SourceChannel != "mini_program" {
t.Fatal("invalid parent", parent)
}
if parent.BatchInfo.Get("requestHash").String() != "hash" || parent.BatchInfo.Get("requestId").String() != "request" {
t.Fatal("request identity lost")
}
if parent.ResultOssId != 0 {
t.Fatal("precheck result file leaked into execution")
}
if parent.StartTime != nil && !parent.StartTime.IsZero() || parent.EndTime != nil && !parent.EndTime.IsZero() {
t.Fatal("prepare times leaked into execution", parent)
}
var children []*entity.BatchMain
if err := dao.BatchMain.Ctx(ctx).Where("source_batch_no", prepare.BatchNo).Scan(&children); err != nil {
t.Fatal(err)
}
if len(children) != 1 || !regexp.MustCompile(`^ALC[0-9]{17}$`).MatchString(children[0].BatchNo) || children[0].BatchInfo.Get("jobRole").String() != "allocate_chunk" {
t.Fatal("invalid child", children)
}
var details []*entity.BatchDetail
if err := dao.BatchDetail.Ctx(ctx).Scan(&details); err != nil {
t.Fatal(err)
}
if len(details) != 1 || details[0].BatchNo != children[0].BatchNo || details[0].Snapshot.Get("targetDeptId").Int64() != 2 {
t.Fatal("invalid execution details", details)
}
for _, snapshot := range []*gjson.Json{parent.BatchInfo, children[0].BatchInfo, details[0].Snapshot} {
if snapshot.Get("sourceDeptName").String() != "划拨单位" || snapshot.Get("targetDeptName").String() != "接收单位" || snapshot.Get("sourceDeptId").Int64() != 1 || snapshot.Get("targetDeptId").Int64() != 2 {
t.Fatal("父子批次和明细单位快照不一致", snapshot)
}
}
var card *entity.TSimCardInfo
if err := dao.TSimCardInfo.Ctx(ctx).Where("id", 1).Scan(&card); err != nil {
t.Fatal(err)
}
if card.DeptId != 1 || card.CreatedDept != 1 || card.AllocationBatchNo != "" {
t.Fatal("queue changed ownership", card)
}
for _, test := range []struct {
value string
allocate bool
count int
}{
{" AR ", true, 1}, {"AR", false, 0}, {"CARD", true, 1}, {"missing", true, 0},
} {
query := NewAppCard().applyCardFilters(dao.TSimCardInfo.Ctx(ctx).Where("tenant_id", "A"), &model.AppCardListParam{CardValue: test.value, AllocateOnly: test.allocate})
if count, err := query.Count(); err != nil || count != test.count {
t.Fatalf("search %q allocate=%v count=%d error=%v", test.value, test.allocate, count, err)
}
}
parentNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixAllocate)
if err != nil {
t.Fatal(err)
}
pending, err := NewAppCard().persistAppAllocatePending(ctx, user, parentNo, 2, batch, "直接异步")
if err != nil {
t.Fatal(err)
}
if pending.BatchType != 3 || pending.BatchAsyncStatus != 1 || pending.ValidCount != 0 || !regexp.MustCompile(`^ALC[0-9]{17}$`).MatchString(pending.BatchNo) {
t.Fatal("direct submission did not create pending task", pending)
}
var directParent *entity.BatchMain
if err := dao.BatchMain.Ctx(ctx).Where("batch_no", parentNo).Scan(&directParent); err != nil || directParent.SourceBatchNo != "" || directParent.BatchInfo.Get("jobRole").String() != "allocate_parent" || !directParent.BatchInfo.Get("splitCompleted").Bool() {
t.Fatal("invalid direct parent protocol", directParent, err)
}
var directChild *entity.BatchMain
if err := dao.BatchMain.Ctx(ctx).Where("source_batch_no", parentNo).Scan(&directChild); err != nil || directChild.BatchInfo.Get("jobRole").String() != "allocate_chunk" || directChild.BatchAsyncStatus != 1 {
t.Fatal("invalid direct child protocol", directChild, err)
}
if err := dao.TSimCardInfo.Ctx(ctx).Where("id", 1).Scan(&card); err != nil || card.DeptId != 1 || card.AllocationBatchNo != "" {
t.Fatal("direct submission changed card ownership", card, err)
}
if _, err := dao.BatchMain.DB().Exec(ctx, `CREATE TRIGGER reject_allocate_child BEFORE INSERT ON batch_main WHEN NEW.source_batch_no = 'ROLLBACK_PARENT' BEGIN SELECT RAISE(ABORT, 'injected child failure'); END`); err != nil {
t.Fatal(err)
}
if _, err := NewAppCard().persistAppAllocatePending(ctx, user, "ROLLBACK_PARENT", 2, batch, ""); err == nil {
t.Fatal("child creation failure was ignored")
}
if count, err := dao.BatchMain.Ctx(ctx).Where("batch_no", "ROLLBACK_PARENT").Count(); err != nil || count != 0 {
t.Fatal("direct submission left orphan parent", count, err)
}
if _, err := dao.BatchMain.DB().Exec(ctx, `DROP TRIGGER reject_allocate_child`); err != nil {
t.Fatal(err)
}
second := &entity.BatchMain{Id: 10, BatchNo: "ALC20261012000002", ExecutionMode: 2}
if _, err := dao.BatchMain.Ctx(ctx).Data(do.BatchMain{Id: 10, BatchNo: second.BatchNo, BatchType: 4, TenantId: "A", CreatedBy: 7}).Insert(); err != nil {
t.Fatal(err)
}
if _, err := dao.BatchDetail.Ctx(ctx).Data(do.BatchDetail{BatchNo: second.BatchNo, BatchType: 4, ItemId: "CARD", TenantId: "A", CreatedBy: 7}).Insert(); err != nil {
t.Fatal(err)
}
injected := errors.New("rollback test")
err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if err := NewAppCard().queueAppAllocateBatch(ctx, tx, user, second, batch, ""); err != nil {
return err
}
return injected
})
if !errors.Is(err, injected) {
t.Fatal("rollback did not return injected failure", err)
}
request := &model.AppCardAllocateBatchSubmitParam{RequestId: "alloc_test_0123456789", CardValues: []string{"CARD"}, TargetDeptId: 2, ExecutionMode: 2}
requestKey, err := allocaterequest.Key(user.TenantId, user.UserId, request.RequestId)
requestNo := "ALC" + requestKey[:60]
if err != nil {
t.Fatal(err)
}
hash := allocaterequest.Fingerprint(request.CardValues, 1, 2, 2, "")
if _, err := dao.BatchMain.Ctx(ctx).Data(do.BatchMain{Id: 20, BatchNo: requestNo, BatchType: 3, SourceBatchNo: "", BatchStatus: 3, BatchAsyncStatus: 3, TenantId: "A", CreatedBy: 7, CreatedDept: 1, TotalCount: 1, TargetDeptId: 2, BatchInfo: gjson.New(map[string]any{"jobRole": "allocate_parent", "requestHash": hash})}).Insert(); err != nil {
t.Fatal(err)
}
if _, err := dao.TSimCardInfo.Ctx(ctx).Where("id", 1).Data(do.TSimCardInfo{DeptId: 2}).Update(); err != nil {
t.Fatal(err)
}
for index := 0; index < 2; index++ {
response, err := NewAppCard().submitAppAllocateRequest(ctx, user, request)
if err != nil || response.BatchNo != requestNo || response.Status != "success" {
t.Fatal("accepted request was revalidated or repeated", response, err)
}
}
request.RequestId = "alloc_missing_0123456789"
if _, err := NewAppCard().submitAppAllocateRequest(ctx, user, request); err == nil {
t.Fatal("missing legacy request created new batch")
}
request.RequestId = "alloc_test_0123456789"
request.TargetDeptId = 3
if _, err := NewAppCard().submitAppAllocateRequest(ctx, user, request); err == nil {
t.Fatal("changed request accepted")
}
if count, err := dao.BatchMain.Ctx(ctx).Where("batch_no", requestNo).Count(); err != nil || count != 1 {
t.Fatal("duplicate request rows", count, err)
}
count, err := dao.BatchMain.Ctx(ctx).Where("source_batch_no", second.BatchNo).Count()
if err != nil || count != 0 {
t.Fatal("rollback left child", count, err)
}
var retained *entity.BatchMain
if err := dao.BatchMain.Ctx(ctx).Where("id", second.Id).Scan(&retained); err != nil || retained.BatchType != 4 {
t.Fatal("rollback changed parent", retained, err)
}
count, err = dao.BatchDetail.Ctx(ctx).Where("batch_no", second.BatchNo).Count()
if err != nil || count != 1 {
t.Fatal("rollback lost original detail", count, err)
}
}