package command import ( "bytes" "testing" ) func TestSendGiftIdempotencyIgnoresGatewayResolvedSnapshots(t *testing.T) { base := SendGift{ Base: Base{CommandID: "combo-1_b1", Room: "room-1", ActorID: 42, DeviceID: "device-first"}, TargetType: "user", TargetUserIDs: []int64{43}, TargetUserID: 43, GiftID: "rose", GiftCount: 9, ComboSessionID: "combo-1", BatchSeq: 1, SenderRegionID: 1001, SenderCountryID: 840, TargetHostScopes: []GiftTargetHostScope{{TargetUserID: 43, TargetIsHost: true, TargetHostRegionID: 1001}}, SenderDisplayProfile: GiftDisplayProfile{UserID: 42, Username: "before"}, } retry := base retry.SenderRegionID = 2002 retry.SenderCountryID = 356 retry.TargetHostScopes = []GiftTargetHostScope{{TargetUserID: 43, TargetIsHost: false}} retry.SenderDisplayProfile = GiftDisplayProfile{UserID: 42, Username: "after"} retry.DeviceID = "device-after-token-rotation" left, err := IdempotencyPayloadForCommand(base) if err != nil { t.Fatalf("base payload: %v", err) } right, err := IdempotencyPayloadForCommand(retry) if err != nil { t.Fatalf("retry payload: %v", err) } if !bytes.Equal(left, right) { t.Fatalf("gateway snapshot drift changed client idempotency semantics:\nleft=%s\nright=%s", left, right) } } func TestSendGiftCommandLogPreservesTrustedDeviceIDAndReadsLegacyPayload(t *testing.T) { original := SendGift{ Base: Base{ AppCode: "lalu", CommandID: "cmd-device-recovery", Room: "room-1", ActorID: 42, SessionID: "sess-not-a-device", DeviceID: "device-auth-session-1", SentAtMS: 1_000, }, TargetType: "user", TargetUserIDs: []int64{43}, TargetUserID: 43, GiftID: "rose", GiftCount: 1, } payload, err := Serialize(original) if err != nil { t.Fatalf("serialize send gift device snapshot: %v", err) } recovered, err := Deserialize(original.Type(), payload) if err != nil { t.Fatalf("deserialize send gift device snapshot: %v", err) } got, ok := recovered.(*SendGift) if !ok || got.DeviceID != "device-auth-session-1" || got.SessionID != "sess-not-a-device" { t.Fatalf("recovered trusted device snapshot mismatch: %#v", recovered) } // 滚动升级前的 command log 不包含 device_id;反序列化必须保持可恢复, // 并留下空值让 fixed_v2 兼容、dynamic_v3 在 owner 处明确拒绝。 legacy, err := Deserialize(original.Type(), []byte(`{"app_code":"lalu","command_id":"cmd-legacy","actor_user_id":42,"room_id":"room-1","session_id":"sess-legacy","target_type":"user","target_user_id":43,"gift_id":"rose","gift_count":1}`)) if err != nil { t.Fatalf("deserialize legacy send gift: %v", err) } legacyGift := legacy.(*SendGift) if legacyGift.DeviceID != "" || legacyGift.SessionID != "sess-legacy" { t.Fatalf("legacy command must not derive device from session: %+v", legacyGift.Base) } } func TestSendGiftIdempotencyKeepsComboBatchIdentity(t *testing.T) { base := SendGift{Base: Base{CommandID: "combo-1_b1", Room: "room-1", ActorID: 42}, TargetType: "user", TargetUserIDs: []int64{43}, TargetUserID: 43, GiftID: "rose", GiftCount: 1, ComboSessionID: "combo-1", BatchSeq: 1} reused := base reused.BatchSeq = 2 left, _ := IdempotencyPayloadForCommand(base) right, _ := IdempotencyPayloadForCommand(reused) if bytes.Equal(left, right) { t.Fatal("same command_id reused for another combo batch was not detected") } }