Files
iotlcm/internal/fullsync/contract/replay_test.go
T
MiFaZhan 2bec257289 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),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

104 lines
4.5 KiB
Go

package contract_test
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/fullsync/contract"
)
func TestReplayRequestIdentity(t *testing.T) {
request := contract.ReplayRequest{SourceRunID: "source", IdempotencyKey: "key", RequestedBy: "operator"}
fingerprint, err := request.Fingerprint()
if err != nil || len(fingerprint) != 64 {
t.Fatalf("fingerprint=%q, err=%v", fingerprint, err)
}
for name, mutate := range map[string]func(*contract.ReplayRequest){
"same": func(*contract.ReplayRequest) {},
"key": func(r *contract.ReplayRequest) { r.IdempotencyKey = "another-key" },
"source": func(r *contract.ReplayRequest) { r.SourceRunID = "another-source" },
"body": func(r *contract.ReplayRequest) { r.RequestedBy = "another-operator" },
} {
copy := request
mutate(&copy)
got, err := copy.Fingerprint()
if err != nil || (got == fingerprint) != (name == "same" || name == "key") {
t.Errorf("%s fingerprint=%s err=%v", name, got, err)
}
}
if got := string(mustJSON(t, request)); got != `{"requestedBy":"operator"}` {
t.Fatalf("path/header leaked into body: %s", got)
}
for _, bad := range []contract.ReplayRequest{
{SourceRunID: "source", RequestedBy: "operator"},
{SourceRunID: "source", IdempotencyKey: "key", RequestedBy: " "},
{SourceRunID: "{source}", IdempotencyKey: "key", RequestedBy: "operator"},
{SourceRunID: "source", IdempotencyKey: "key\r\ninjected", RequestedBy: "operator"},
{SourceRunID: "source", IdempotencyKey: strings.Repeat("x", 257), RequestedBy: "operator"},
} {
if _, err := bad.Fingerprint(); !errors.Is(err, contract.ErrInvalidArgument) {
t.Errorf("bad request returned %v", err)
}
}
}
func TestActionProgressAndReceiptValidation(t *testing.T) {
for _, progress := range []contract.ReplayProgress{
{DiscoveredCount: -1}, {DiscoveredCount: 1, EnqueuedCount: 2},
{DiscoveredCount: 2, EnqueuedCount: 1, CleanedCount: 2}, {FailureCount: -1},
} {
if err := progress.Validate(); !errors.Is(err, contract.ErrInvalidArgument) {
t.Fatalf("bad progress %+v returned %v", progress, err)
}
}
now := time.Now().UTC()
action := contract.ReplayAction{ActionID: "action", SourceRunID: "source", TargetRunID: "target", RequestedBy: "operator", ReplayCycle: 1, Status: contract.ActionAccepted}
if err := action.Validate(); err != nil {
t.Fatal(err)
}
action.Status = contract.ActionCompleted
if err := action.Validate(); !errors.Is(err, contract.ErrInvalidArgument) {
t.Fatalf("completed action without receipt returned %v", err)
}
action.Receipt = &contract.ArchiveReceipt{Version: 1, Path: "receipt.json", Checksum: strings.Repeat("a", 64), ArchivedAt: now}
action.CompletedAt = &now
if err := action.Validate(); err != nil {
t.Fatal(err)
}
action.Receipt.Checksum = "not-a-checksum"
if err := action.Validate(); !errors.Is(err, contract.ErrInvalidArgument) {
t.Fatalf("bad receipt returned %v", err)
}
}
func TestActionWireFormatHidesInternalState(t *testing.T) {
action := contract.ReplayAction{
ActionID: "action", SourceRunID: "source", TargetRunID: "target", RequestedBy: "operator", ReplayCycle: 1,
Status: contract.ActionCleanupPending, IdempotencyKey: "secret", Fingerprint: "internal-fingerprint",
LeaseOwner: "worker", LeaseToken: "token", LeaseUntil: time.Now(), Revision: 9,
Progress: contract.ReplayProgress{ScanCursor: "opaque", DiscoveredCount: 10, EnqueuedCount: 8, CleanedCount: 7, FailureCount: 2},
}
data := mustJSON(t, action)
for _, hidden := range []string{"idempotency", "fingerprint", "lease", "revision", "secret", "token"} {
if strings.Contains(strings.ToLower(string(data)), hidden) {
t.Errorf("internal %s leaked: %s", hidden, data)
}
}
var decoded contract.ReplayAction
if err := json.Unmarshal(data, &decoded); err != nil || !reflect.DeepEqual(decoded.Progress, action.Progress) || decoded.Status != contract.ActionCleanupPending {
t.Fatalf("action roundtrip lost state: %+v, %v", decoded, err)
}
statusURL, err := contract.ActionStatusURL("action%1")
if err != nil || statusURL != "/internal/v1/full-sync-actions/action%251" {
t.Fatalf("status URL=%s, %v", statusURL, err)
}
accepted := contract.ReplayAccepted{ActionID: "action", TargetRunID: "target", Status: contract.ActionAccepted, StatusURL: "/internal/v1/full-sync-actions/action"}
if got := string(mustJSON(t, accepted)); got != `{"actionId":"action","targetRunId":"target","status":"accepted","statusUrl":"/internal/v1/full-sync-actions/action"}` {
t.Fatalf("202 contract changed: %s", got)
}
}