chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/batch"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestRestartResumeBeforeScheduledTimeIsRescheduled(t *testing.T) {
|
||||
now := gtime.New("2026-10-10 13:46:42")
|
||||
repo := &fakeRestartRepository{
|
||||
main: restartMain(consts.RestartPhaseWaitingResume, gjson.New(map[string]any{
|
||||
"restart": map[string]any{"resumeAt": "2026-10-10 13:46:44"},
|
||||
})),
|
||||
}
|
||||
publisher := &recordingRestartPublisher{}
|
||||
executor := NewRestartExecutorWithPublisher(repo, &fakeUpstream{}, publisher.PublishDelayed).(*restartExecutor)
|
||||
executor.now = func() *gtime.Time { return now }
|
||||
|
||||
ok, err := executor.CanExecuteProcessing(context.Background(), repo.main)
|
||||
if ok || err != nil {
|
||||
t.Fatalf("CanExecuteProcessing() = %t, %v; want false, nil", ok, err)
|
||||
}
|
||||
if publisher.calls != 1 || publisher.delay != 2*time.Second {
|
||||
t.Fatalf("publisher calls=%d delay=%s; want one call with 2s delay", publisher.calls, publisher.delay)
|
||||
}
|
||||
if publisher.message != (batch.Message{BatchNo: repo.main.BatchNo, BatchType: repo.main.BatchType}) {
|
||||
t.Fatalf("rescheduled message = %+v", publisher.message)
|
||||
}
|
||||
if repo.claimed || repo.finalized {
|
||||
t.Fatal("early message must not claim or finalize the batch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartResumeRescheduleFailureIsReturned(t *testing.T) {
|
||||
now := gtime.New("2026-10-10 13:46:42")
|
||||
main := restartMain(consts.RestartPhaseWaitingResume, gjson.New(map[string]any{
|
||||
"restart": map[string]any{"resumeAt": "2026-10-10 13:46:44"},
|
||||
}))
|
||||
publishErr := errors.New("publish failed")
|
||||
publisher := func(context.Context, string, string, time.Duration, any) error { return publishErr }
|
||||
executor := NewRestartExecutorWithPublisher(&fakeRestartRepository{main: main}, &fakeUpstream{}, publisher).(*restartExecutor)
|
||||
executor.now = func() *gtime.Time { return now }
|
||||
ok, err := executor.CanExecuteProcessing(context.Background(), main)
|
||||
if ok || !errors.Is(err, publishErr) || batch.IsRetryable(err) {
|
||||
t.Fatalf("CanExecuteProcessing() = %t, %v; want publish error without execution retry flag", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartStopValidationFailureSkipsUpstream(t *testing.T) {
|
||||
task := restartStopTask()
|
||||
task.Details = append(task.Details, &Detail{
|
||||
ID: 2,
|
||||
ICCID: "iccid-2",
|
||||
Authorized: false,
|
||||
Card: &Card{ID: 12, ChanKey: "channel", BossCardStatus: consts.SimBossCardStatusActivated},
|
||||
})
|
||||
repo := &fakeRestartRepository{
|
||||
main: restartMain(consts.RestartPhaseCreated, nil),
|
||||
stopTask: task,
|
||||
}
|
||||
upstream := &fakeUpstream{}
|
||||
executor := NewRestartExecutorWithPublisher(repo, upstream, fakeRestartPublisher(t)).(*restartExecutor)
|
||||
|
||||
if err := executor.Execute(context.Background(), "RST-1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if upstream.calls != 0 {
|
||||
t.Fatalf("upstream calls = %d, want 0", upstream.calls)
|
||||
}
|
||||
if repo.validationStage != restartStageStop || !repo.finalized {
|
||||
t.Fatalf("validation/finalize state = stage:%q finalized:%t", repo.validationStage, repo.finalized)
|
||||
}
|
||||
if len(repo.saved) != 0 || repo.waited {
|
||||
t.Fatalf("unexpected stop result or wait: saved=%d waited=%t", len(repo.saved), repo.waited)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartStopSuccessSchedulesDelayedResume(t *testing.T) {
|
||||
now := gtime.NewFromStr("2026-07-20 10:00:00")
|
||||
task := restartStopTask()
|
||||
task.Batch.BatchInfo = gjson.New(map[string]any{
|
||||
"resumeDelaySeconds": 1,
|
||||
"restart": map[string]any{"phase": consts.RestartPhaseCreated},
|
||||
})
|
||||
repo := &fakeRestartRepository{
|
||||
main: restartMain(consts.RestartPhaseCreated, gjson.New(map[string]any{"resumeDelaySeconds": 1})),
|
||||
stopTask: task,
|
||||
}
|
||||
upstream := &fakeUpstream{response: []model.SimBaseRes[model.SimCardStatusChangeOutputItem]{
|
||||
{Code: 0, Message: "ok", ResData: model.SimCardStatusChangeOutputItem{OrderNum: "stop-order"}},
|
||||
}}
|
||||
publisher := &recordingRestartPublisher{}
|
||||
executor := NewRestartExecutorWithPublisher(repo, upstream, publisher.PublishDelayed).(*restartExecutor)
|
||||
executor.now = func() *gtime.Time { return now }
|
||||
|
||||
if err := executor.Execute(context.Background(), "RST-2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if upstream.calls != 1 || upstream.input.ReqData.OperType != consts.SimCardStatusOperTypeStop {
|
||||
t.Fatalf("upstream stop input = %+v calls=%d", upstream.input, upstream.calls)
|
||||
}
|
||||
if len(repo.saved) != 1 || repo.saved[0].stage != restartStageStop || !repo.saved[0].outcome.Success {
|
||||
t.Fatalf("saved stop results = %+v", repo.saved)
|
||||
}
|
||||
if !repo.waited || repo.resumeAt.String() != now.Add(time.Second).String() {
|
||||
t.Fatalf("resume wait = waited:%t resumeAt:%v", repo.waited, repo.resumeAt)
|
||||
}
|
||||
message, ok := publisher.message.(batch.Message)
|
||||
if !ok || publisher.calls != 1 || publisher.delay != time.Second || message.BatchType != consts.BatchTypeSimCardRestart {
|
||||
t.Fatalf("publisher = %+v", publisher)
|
||||
}
|
||||
if repo.finalized {
|
||||
t.Fatal("batch must not finalize before resume when stop succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartResumeClaimLostSkipsDuplicateUpstream(t *testing.T) {
|
||||
now := gtime.NewFromStr("2026-07-20 10:05:00")
|
||||
repo := &fakeRestartRepository{
|
||||
main: restartMain(consts.RestartPhaseWaitingResume, gjson.New(map[string]any{"restart": map[string]any{"resumeAt": now.String()}})),
|
||||
claimResult: false,
|
||||
resumeTask: restartResumeTask(),
|
||||
}
|
||||
upstream := &fakeUpstream{}
|
||||
executor := NewRestartExecutorWithPublisher(repo, upstream, fakeRestartPublisher(t)).(*restartExecutor)
|
||||
executor.now = func() *gtime.Time { return now }
|
||||
|
||||
if err := executor.Execute(context.Background(), "RST-3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !repo.claimed {
|
||||
t.Fatal("resume CAS was not attempted")
|
||||
}
|
||||
if upstream.calls != 0 || repo.loadedResume || repo.finalized {
|
||||
t.Fatalf("duplicate was not skipped: upstream=%d loadedResume=%t finalized=%t", upstream.calls, repo.loadedResume, repo.finalized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartResumeExecutesAfterClaim(t *testing.T) {
|
||||
now := gtime.NewFromStr("2026-07-20 10:05:00")
|
||||
repo := &fakeRestartRepository{
|
||||
main: restartMain(consts.RestartPhaseWaitingResume, gjson.New(map[string]any{"restart": map[string]any{"resumeAt": now.String()}})),
|
||||
claimResult: true,
|
||||
resumeTask: restartResumeTask(),
|
||||
}
|
||||
upstream := &fakeUpstream{response: []model.SimBaseRes[model.SimCardStatusChangeOutputItem]{
|
||||
{Code: 0, Message: "ok", ResData: model.SimCardStatusChangeOutputItem{OrderNum: "resume-order"}},
|
||||
}}
|
||||
executor := NewRestartExecutorWithPublisher(repo, upstream, fakeRestartPublisher(t)).(*restartExecutor)
|
||||
executor.now = func() *gtime.Time { return now }
|
||||
|
||||
if ok, err := executor.CanExecuteProcessing(context.Background(), repo.main); err != nil || !ok {
|
||||
t.Fatalf("CanExecuteProcessing() = %t, %v; want true, nil", ok, err)
|
||||
}
|
||||
if err := executor.Execute(context.Background(), "RST-4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if upstream.calls != 1 || upstream.input.ReqData.OperType != consts.SimCardStatusOperTypeStart {
|
||||
t.Fatalf("upstream resume input = %+v calls=%d", upstream.input, upstream.calls)
|
||||
}
|
||||
if len(repo.saved) != 1 || repo.saved[0].stage != restartStageResume || !repo.saved[0].outcome.Success {
|
||||
t.Fatalf("saved resume results = %+v", repo.saved)
|
||||
}
|
||||
if !repo.finalized {
|
||||
t.Fatal("resume phase did not finalize the RST batch")
|
||||
}
|
||||
}
|
||||
|
||||
func restartMain(phase string, info *gjson.Json) *entity.BatchMain {
|
||||
data := map[string]any{"restart": map[string]any{"phase": phase}}
|
||||
if info != nil {
|
||||
for key, value := range info.Map() {
|
||||
if key == "restart" {
|
||||
restart := data["restart"].(map[string]any)
|
||||
for restartKey, restartValue := range gjson.New(value).Map() {
|
||||
restart[restartKey] = restartValue
|
||||
}
|
||||
continue
|
||||
}
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
return &entity.BatchMain{
|
||||
BatchNo: "RST",
|
||||
BatchType: consts.BatchTypeSimCardRestart,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
|
||||
BatchInfo: gjson.New(data),
|
||||
}
|
||||
}
|
||||
|
||||
func restartStopTask() *Task {
|
||||
return &Task{
|
||||
Batch: Batch{
|
||||
BatchNo: "RST",
|
||||
BatchType: consts.BatchTypeSimCardRestart,
|
||||
BatchInfo: gjson.New(map[string]any{"restart": map[string]any{"phase": consts.RestartPhaseCreated}}),
|
||||
},
|
||||
Operation: Operation{OperType: consts.SimCardStatusOperTypeStop, ExpectedStatus: consts.SimBossCardStatusActivated, TargetStatus: consts.SimBossCardStatusStopped},
|
||||
Details: []*Detail{
|
||||
{ID: 1, ICCID: "iccid-1", Authorized: true, Card: &Card{ID: 11, ChanKey: "channel", BossCardStatus: consts.SimBossCardStatusActivated}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func restartResumeTask() *Task {
|
||||
return &Task{
|
||||
Batch: Batch{BatchNo: "RST", BatchType: consts.BatchTypeSimCardRestart},
|
||||
Operation: Operation{OperType: consts.SimCardStatusOperTypeStart, ExpectedStatus: consts.SimBossCardStatusStopped, TargetStatus: consts.SimBossCardStatusActivated},
|
||||
Details: []*Detail{
|
||||
{ID: 1, ICCID: "iccid-1", Authorized: true, Card: &Card{ID: 11, ChanKey: "channel", BossCardStatus: consts.SimBossCardStatusStopped}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRestartRepository struct {
|
||||
main *entity.BatchMain
|
||||
stopTask *Task
|
||||
resumeTask *Task
|
||||
validationStage string
|
||||
saved []restartSavedResult
|
||||
waited bool
|
||||
resumeAt *gtime.Time
|
||||
claimed bool
|
||||
claimResult bool
|
||||
loadedResume bool
|
||||
finalized bool
|
||||
}
|
||||
|
||||
type restartSavedResult struct {
|
||||
stage string
|
||||
outcome Outcome
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) FindRestartMain(context.Context, string) (*entity.BatchMain, error) {
|
||||
return r.main, nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) LoadRestartStop(context.Context, string) (*Task, error) {
|
||||
return r.stopTask, nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) LoadRestartResume(context.Context, string) (*Task, error) {
|
||||
r.loadedResume = true
|
||||
return r.resumeTask, nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) SaveRestartValidationFailure(_ context.Context, _ *Task, stage string) error {
|
||||
r.validationStage = stage
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) SaveRestartResult(_ context.Context, _ *Task, detail *Detail, outcome Outcome, stage string) (Outcome, error) {
|
||||
detail.ApplyOutcome(outcome)
|
||||
r.saved = append(r.saved, restartSavedResult{stage: stage, outcome: outcome})
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) WaitRestartResume(_ context.Context, _ *Task, resumeAt *gtime.Time) error {
|
||||
r.waited = true
|
||||
r.resumeAt = resumeAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) ClaimRestartResume(context.Context, string, *gtime.Time) (bool, error) {
|
||||
r.claimed = true
|
||||
return r.claimResult, nil
|
||||
}
|
||||
|
||||
func (r *fakeRestartRepository) FinalizeRestart(context.Context, string) error {
|
||||
r.finalized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type recordingRestartPublisher struct {
|
||||
calls int
|
||||
delay time.Duration
|
||||
message any
|
||||
}
|
||||
|
||||
func (p *recordingRestartPublisher) PublishDelayed(_ context.Context, _, _ string, delay time.Duration, data any) error {
|
||||
p.calls++
|
||||
p.delay = delay
|
||||
p.message = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func fakeRestartPublisher(t *testing.T) RestartPublisher {
|
||||
t.Helper()
|
||||
return func(context.Context, string, string, time.Duration, any) error {
|
||||
t.Fatal("publisher must not be called")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user