package transcoder import ( "bytes" "reflect" "strconv" "strings" "testing" model "galaxy/model/report" fbs "galaxy/schema/fbs/battle" flatbuffers "github.com/google/flatbuffers/go" "github.com/google/uuid" ) func TestBattleReportToPayloadAndPayloadToBattleReportRoundTrip(t *testing.T) { t.Parallel() source := sampleBattleReport() payload, err := BattleReportToPayload(source) if err != nil { t.Fatalf("encode battle report payload: %v", err) } decoded, err := PayloadToBattleReport(payload) if err != nil { t.Fatalf("decode battle report payload: %v", err) } expected := battleReportWireClone(t, source) if !reflect.DeepEqual(expected, decoded) { t.Fatalf("round-trip mismatch\nexpected: %#v\ndecoded: %#v", expected, decoded) } } func TestBattleReportToPayloadNilReport(t *testing.T) { t.Parallel() _, err := BattleReportToPayload(nil) if err == nil { t.Fatal("expected error for nil battle report") } } func TestPayloadToBattleReportEmptyData(t *testing.T) { t.Parallel() _, err := PayloadToBattleReport(nil) if err == nil { t.Fatal("expected error for empty payload") } } func TestPayloadToBattleReportGarbageDataDoesNotPanic(t *testing.T) { t.Parallel() _, err := PayloadToBattleReport([]byte{0x01, 0x02, 0x03}) if err == nil { t.Fatal("expected error for malformed payload") } } func TestPayloadToBattleReportMissingID(t *testing.T) { t.Parallel() payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { fbs.BattleReportStart(builder) fbs.BattleReportAddPlanet(builder, 7) return fbs.BattleReportEnd(builder) }) _, err := PayloadToBattleReport(payload) if err == nil { t.Fatal("expected error for missing battle report id") } if !strings.Contains(err.Error(), "id is missing") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToBattleReportMissingRaceValue(t *testing.T) { t.Parallel() payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { fbs.RaceEntryStart(builder) fbs.RaceEntryAddKey(builder, 1) race := fbs.RaceEntryEnd(builder) fbs.BattleReportStartRacesVector(builder, 1) builder.PrependUOffsetT(race) races := builder.EndVector(1) fbs.BattleReportStart(builder) fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2)) fbs.BattleReportAddRaces(builder, races) return fbs.BattleReportEnd(builder) }) _, err := PayloadToBattleReport(payload) if err == nil { t.Fatal("expected error for missing race value") } if !strings.Contains(err.Error(), "race value is missing") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToBattleReportMissingShipValue(t *testing.T) { t.Parallel() payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { fbs.ShipEntryStart(builder) fbs.ShipEntryAddKey(builder, 1) ship := fbs.ShipEntryEnd(builder) fbs.BattleReportStartShipsVector(builder, 1) builder.PrependUOffsetT(ship) ships := builder.EndVector(1) fbs.BattleReportStart(builder) fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2)) fbs.BattleReportAddShips(builder, ships) return fbs.BattleReportEnd(builder) }) _, err := PayloadToBattleReport(payload) if err == nil { t.Fatal("expected error for missing ship value") } if !strings.Contains(err.Error(), "ship value is missing") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToBattleReportOverflowInt(t *testing.T) { t.Parallel() if strconv.IntSize == 64 { t.Skip("int overflow from int64 is not possible on 64-bit runtime") } maxInt := int(^uint(0) >> 1) overflowValue := int64(maxInt) + 1 payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { fbs.BattleActionReportStart(builder) fbs.BattleActionReportAddAttacker(builder, overflowValue) protocol := fbs.BattleActionReportEnd(builder) fbs.BattleReportStartProtocolVector(builder, 1) builder.PrependUOffsetT(protocol) protocolVector := builder.EndVector(1) fbs.BattleReportStart(builder) fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2)) fbs.BattleReportAddProtocol(builder, protocolVector) return fbs.BattleReportEnd(builder) }) _, err := PayloadToBattleReport(payload) if err == nil { t.Fatal("expected overflow error") } if !strings.Contains(err.Error(), "overflows int") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToBattleReportOverflowUint(t *testing.T) { t.Parallel() if strconv.IntSize == 64 { t.Skip("uint overflow from uint64 is not possible on 64-bit runtime") } maxUint := uint64(^uint(0)) overflowValue := maxUint + 1 payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { fbs.BattleReportStart(builder) fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2)) fbs.BattleReportAddPlanet(builder, overflowValue) return fbs.BattleReportEnd(builder) }) _, err := PayloadToBattleReport(payload) if err == nil { t.Fatal("expected overflow error") } if !strings.Contains(err.Error(), "overflows uint") { t.Fatalf("unexpected error: %v", err) } } func TestBattleReportToPayloadDeterministicMapEncoding(t *testing.T) { t.Parallel() report := sampleBattleReport() firstPayload, err := BattleReportToPayload(report) if err != nil { t.Fatalf("encode battle report payload: %v", err) } for i := 0; i < 20; i++ { nextPayload, nextErr := BattleReportToPayload(report) if nextErr != nil { t.Fatalf("encode battle report payload #%d: %v", i+2, nextErr) } if !bytes.Equal(firstPayload, nextPayload) { t.Fatalf("payload differs between runs at iteration %d", i+2) } } } func sampleBattleReport() *model.BattleReport { return &model.BattleReport{ ID: uuid.MustParse("44444444-4444-4444-4444-444444444444"), Planet: 12, PlanetName: "Nexus", Races: map[int]uuid.UUID{ 2: uuid.MustParse("55555555-5555-5555-5555-555555555555"), 1: uuid.MustParse("66666666-6666-6666-6666-666666666666"), }, Ships: map[int]model.BattleReportGroup{ 3: { InBattle: true, Number: 20, NumberLeft: 7, LoadQuantity: model.Float(3.5), Tech: map[string]model.Float{"WEAPONS": model.Float(2.0), "DRIVE": model.Float(1.5)}, Race: "Terrans", ClassName: "Frigate", LoadType: "MAT", }, 1: { InBattle: false, Number: 15, NumberLeft: 15, LoadQuantity: model.Float(0.0), Tech: map[string]model.Float{"CARGO": model.Float(1.25), "SHIELDS": model.Float(1.75)}, Race: "Martians", ClassName: "Destroyer", LoadType: "CAP", }, }, Protocol: []model.BattleActionReport{ {Attacker: 1, AttackerShipClass: 3, Defender: 2, DefenderShipClass: 1, Destroyed: true}, {Attacker: 2, AttackerShipClass: 1, Defender: 1, DefenderShipClass: 3, Destroyed: false}, }, } } func battleReportWireClone(t *testing.T, source *model.BattleReport) *model.BattleReport { t.Helper() data, err := source.MarshalBinary() if err != nil { t.Fatalf("marshal source battle report: %v", err) } result := new(model.BattleReport) if err := result.UnmarshalBinary(data); err != nil { t.Fatalf("unmarshal source battle report clone: %v", err) } return result } func buildBattleReportPayload(build func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte { builder := flatbuffers.NewBuilder(256) offset := build(builder) fbs.FinishBattleReportBuffer(builder, offset) return builder.FinishedBytes() }