package protocol import ( "encoding/json" "testing" ) func TestParseJoinRoundtrip(t *testing.T) { opcode, args, err := ParseMessage([]byte(`[1,"game-token","user-token"]`)) if err != nil { t.Fatalf("parse: %v", err) } if opcode != OpJoin { t.Fatalf("opcode = %d, want %d", opcode, OpJoin) } gt, ut, err := ParseJoin(args) if err != nil { t.Fatalf("parse join: %v", err) } if gt != "game-token" || ut != "user-token" { t.Fatalf("got (%q, %q)", gt, ut) } } func TestParseMessageErrors(t *testing.T) { for _, data := range []string{`{}`, `[]`, `["1"]`, `not json`} { if _, _, err := ParseMessage([]byte(data)); err == nil { t.Fatalf("expected error for %s", data) } } } func TestBuildEnd(t *testing.T) { data := BuildEnd(7, EndCodeJoinTimeout, "join timeout exceeded") var v []any if err := json.Unmarshal(data, &v); err != nil { t.Fatalf("unmarshal: %v", err) } want := []any{float64(2), float64(7), float64(-1), "join timeout exceeded"} if len(v) != len(want) { t.Fatalf("got %v, want %v", v, want) } for i := range want { if v[i] != want[i] { t.Fatalf("got %v, want %v", v, want) } } } func TestBuildStateAndAck(t *testing.T) { if got := string(BuildState(3, StatePlacingShips)); got != `[3,3,1]` { t.Fatalf("BuildState = %s", got) } if got := string(BuildAck(3)); got != `[4,3]` { t.Fatalf("BuildAck = %s", got) } } func TestParseAck(t *testing.T) { _, args, err := ParseMessage([]byte(`[4,12]`)) if err != nil { t.Fatalf("parse: %v", err) } seq, err := ParseAck(args) if err != nil { t.Fatalf("parse ack: %v", err) } if seq != 12 { t.Fatalf("seq = %d, want 12", seq) } } func validTestShips() []Ship { return []Ship{ // 4 size 1 {Orientation: OrientationHorizontal, X: 9, Y: 4, Size: 1}, {Orientation: OrientationVertical, X: 7, Y: 4, Size: 1}, {Orientation: OrientationHorizontal, X: 5, Y: 4, Size: 1}, {Orientation: OrientationHorizontal, X: 3, Y: 4, Size: 1}, // 3 size 2 {Orientation: OrientationHorizontal, X: 0, Y: 4, Size: 2}, {Orientation: OrientationHorizontal, X: 7, Y: 2, Size: 2}, {Orientation: OrientationHorizontal, X: 4, Y: 2, Size: 2}, // 2 size 3 {Orientation: OrientationHorizontal, X: 0, Y: 2, Size: 3}, {Orientation: OrientationHorizontal, X: 5, Y: 0, Size: 3}, // 1 size 4 {Orientation: OrientationHorizontal, X: 0, Y: 0, Size: 4}, } } func TestPlaceShipsRoundtrip(t *testing.T) { ships := validTestShips() data := BuildPlaceShips(ships) opcode, args, err := ParseMessage(data) if err != nil { t.Fatalf("ParseMessage: %v", err) } if opcode != OpPlaceShips { t.Fatalf("opcode = %d, want %d", opcode, OpPlaceShips) } parsed, err := ParsePlaceShips(args) if err != nil { t.Fatalf("ParsePlaceShips: %v", err) } if len(parsed) != len(ships) { t.Fatalf("got %d ships, want %d", len(parsed), len(ships)) } for i := range ships { if parsed[i] != ships[i] { t.Fatalf("ship %d = %+v, want %+v", i, parsed[i], ships[i]) } } if err := ValidateShips(parsed); err != nil { t.Fatalf("ValidateShips: %v", err) } } func TestParsePlaceShipsErrors(t *testing.T) { t.Run("wrong argument count", func(t *testing.T) { args := make([]json.RawMessage, 29) for i := range args { args[i] = json.RawMessage(`0`) } if _, err := ParsePlaceShips(args); err == nil { t.Fatal("expected error for 29 arguments") } }) t.Run("orientation not boolean", func(t *testing.T) { ships := validTestShips() raw := make([]any, 31) raw[0] = OpPlaceShips for i, s := range ships { raw[1+i*3] = s.Orientation raw[1+i*3+1] = s.X raw[1+i*3+2] = s.Y } raw[1] = "invalid_orientation" // string instead of bool data, _ := json.Marshal(raw) _, args, _ := ParseMessage(data) if _, err := ParsePlaceShips(args); err == nil { t.Fatal("expected error for non-boolean orientation") } }) t.Run("x not integer", func(t *testing.T) { ships := validTestShips() raw := make([]any, 31) raw[0] = OpPlaceShips for i, s := range ships { raw[1+i*3] = s.Orientation raw[1+i*3+1] = s.X raw[1+i*3+2] = s.Y } raw[2] = 1.5 // float instead of integer data, _ := json.Marshal(raw) _, args, _ := ParseMessage(data) if _, err := ParsePlaceShips(args); err == nil { t.Fatal("expected error for float x") } }) t.Run("y not integer", func(t *testing.T) { ships := validTestShips() raw := make([]any, 31) raw[0] = OpPlaceShips for i, s := range ships { raw[1+i*3] = s.Orientation raw[1+i*3+1] = s.X raw[1+i*3+2] = s.Y } raw[3] = "zero" // string instead of integer data, _ := json.Marshal(raw) _, args, _ := ParseMessage(data) if _, err := ParsePlaceShips(args); err == nil { t.Fatal("expected error for string y") } }) } func TestValidateShips(t *testing.T) { t.Run("valid placement passes", func(t *testing.T) { ships := validTestShips() if err := ValidateShips(ships); err != nil { t.Fatalf("unexpected validation error: %v", err) } }) t.Run("wrong ship count", func(t *testing.T) { ships := validTestShips()[:9] if err := ValidateShips(ships); err == nil { t.Fatal("expected error for 9 ships") } }) t.Run("wrong ship size", func(t *testing.T) { ships := validTestShips() ships[0].Size = 2 // ship 0 must have size 1 if err := ValidateShips(ships); err == nil { t.Fatal("expected error for wrong ship size") } }) t.Run("negative coordinate", func(t *testing.T) { ships := validTestShips() ships[0].X = -1 if err := ValidateShips(ships); err == nil { t.Fatal("expected error for negative X") } }) t.Run("out of bounds horizontal", func(t *testing.T) { ships := validTestShips() // Ship 9 has size 4; at X=7, cells are 7, 8, 9, 10 ships[9].X = 7 if err := ValidateShips(ships); err == nil { t.Fatal("expected error for horizontal ship sticking out of bounds") } }) t.Run("out of bounds vertical", func(t *testing.T) { ships := validTestShips() // Ship 9 has size 4, vertical at Y=7: cells are 7, 8, 9, 10 ships[9].Orientation = OrientationVertical ships[9].Y = 7 if err := ValidateShips(ships); err == nil { t.Fatal("expected error for vertical ship sticking out of bounds") } }) t.Run("overlapping ships", func(t *testing.T) { ships := validTestShips() // Place ship 0 at exact same cell as ship 1 ships[0].X = ships[1].X ships[0].Y = ships[1].Y if err := ValidateShips(ships); err == nil { t.Fatal("expected error for overlapping ships") } }) t.Run("orthogonal touching", func(t *testing.T) { ships := validTestShips() // Ship 0 is at (9, 4), place ship 1 at (9, 5) -> touches orthogonally ships[1].X = 9 ships[1].Y = 5 if err := ValidateShips(ships); err == nil { t.Fatal("expected error for orthogonally touching ships") } }) t.Run("diagonal touching", func(t *testing.T) { ships := validTestShips() // Ship 0 is at (9, 4), place ship 1 at (8, 5) -> touches diagonally ships[1].X = 8 ships[1].Y = 5 if err := ValidateShips(ships); err == nil { t.Fatal("expected error for diagonally touching ships") } }) } func TestFireRoundtrip(t *testing.T) { data := BuildFire(3, 7) opcode, args, err := ParseMessage(data) if err != nil { t.Fatalf("ParseMessage: %v", err) } if opcode != OpFire { t.Fatalf("opcode = %d, want %d", opcode, OpFire) } x, y, err := ParseFire(args) if err != nil { t.Fatalf("ParseFire: %v", err) } if x != 3 || y != 7 { t.Fatalf("got (%d, %d), want (3, 7)", x, y) } } func TestParseFireErrors(t *testing.T) { t.Run("wrong argument count", func(t *testing.T) { _, args, _ := ParseMessage([]byte(`[6, 1]`)) if _, _, err := ParseFire(args); err == nil { t.Fatal("expected error for 1 argument") } _, args3, _ := ParseMessage([]byte(`[6, 1, 2, 3]`)) if _, _, err := ParseFire(args3); err == nil { t.Fatal("expected error for 3 arguments") } }) t.Run("x not integer", func(t *testing.T) { _, args, _ := ParseMessage([]byte(`[6, "a", 2]`)) if _, _, err := ParseFire(args); err == nil { t.Fatal("expected error for string x") } }) t.Run("y not integer", func(t *testing.T) { _, args, _ := ParseMessage([]byte(`[6, 1, true]`)) if _, _, err := ParseFire(args); err == nil { t.Fatal("expected error for bool y") } }) } func TestFireResultRoundtrip(t *testing.T) { data := BuildFireResult(5, FireResultSunk) opcode, args, err := ParseMessage(data) if err != nil { t.Fatalf("ParseMessage: %v", err) } if opcode != OpFireResult { t.Fatalf("opcode = %d, want %d", opcode, OpFireResult) } seq, result, err := ParseFireResult(args) if err != nil { t.Fatalf("ParseFireResult: %v", err) } if seq != 5 || result != FireResultSunk { t.Fatalf("got seq=%d, result=%d, want seq=5, result=%d", seq, result, FireResultSunk) } } func TestParseFireResultErrors(t *testing.T) { t.Run("wrong argument count", func(t *testing.T) { _, args, _ := ParseMessage([]byte(`[7, 1]`)) if _, _, err := ParseFireResult(args); err == nil { t.Fatal("expected error for 1 argument") } }) t.Run("seq not int64", func(t *testing.T) { _, args, _ := ParseMessage([]byte(`[7, "str", 1]`)) if _, _, err := ParseFireResult(args); err == nil { t.Fatal("expected error for string seq") } }) t.Run("result not int", func(t *testing.T) { _, args, _ := ParseMessage([]byte(`[7, 1, "hit"]`)) if _, _, err := ParseFireResult(args); err == nil { t.Fatal("expected error for string result") } }) }