262 lines
12 KiB
Go
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)
|
|
}
|
|
}
|