package protocol import ( "encoding/json" "testing" "time" ) func TestEnvelopeValidate_AgentHello(t *testing.T) { payload := AgentHelloPayload{ NodeID: "node-01", AgentVersion: "0.1.0", ProtocolVersion: CurrentProtocolVersion, } raw, err := json.Marshal(payload) if err != nil { t.Fatal(err) } env := &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-001", Type: TypeAgentHello, Timestamp: time.Now().UTC(), Payload: raw, } if err := env.Validate(); err != nil { t.Fatalf("expected valid envelope, got %v", err) } if !env.Type.IsAgentToControl() { t.Fatal("agent.hello should be agent-to-control") } } func TestEnvelopeValidate_ServerStart(t *testing.T) { payload := ServerStartPayload{ OperationMeta: OperationMeta{ ServerID: "srv-01", Generation: 4, }, DesiredGeneration: 4, } raw, err := json.Marshal(payload) if err != nil { t.Fatal(err) } env := &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-002", CorrelationID: "corr-002", Type: TypeServerStart, Timestamp: time.Now().UTC(), Payload: raw, } if err := env.Validate(); err != nil { t.Fatalf("expected valid envelope, got %v", err) } if !env.Type.IsControlToAgent() { t.Fatal("server.start should be control-to-agent") } } func TestEnvelopeValidate_RejectsInvalid(t *testing.T) { tests := []struct { name string env Envelope }{ { name: "missing message id", env: Envelope{ ProtocolVersion: 1, Type: TypeAgentHeartbeat, Timestamp: time.Now().UTC(), Payload: json.RawMessage(`{"nodeId":"n1"}`), }, }, { name: "unknown type", env: Envelope{ ProtocolVersion: 1, MessageID: "m1", Type: "agent.unknown", Timestamp: time.Now().UTC(), Payload: json.RawMessage(`{}`), }, }, { name: "non-utc timestamp", env: Envelope{ ProtocolVersion: 1, MessageID: "m1", Type: TypeAgentHeartbeat, Timestamp: time.Now().Local(), Payload: json.RawMessage(`{"nodeId":"n1"}`), }, }, { name: "invalid payload json", env: Envelope{ ProtocolVersion: 1, MessageID: "m1", Type: TypeAgentHeartbeat, Timestamp: time.Now().UTC(), Payload: json.RawMessage(`{not-json`), }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if err := tc.env.Validate(); err == nil { t.Fatal("expected validation error") } }) } } func TestDecodeEnvelope(t *testing.T) { data := []byte(`{ "protocolVersion": 1, "messageId": "01HXYZ", "type": "agent.heartbeat", "timestamp": "2026-01-01T00:00:00Z", "payload": {"nodeId":"node-01","runningServers":0} }`) env, err := DecodeEnvelope(data) if err != nil { t.Fatalf("decode failed: %v", err) } if env.Type != TypeAgentHeartbeat { t.Fatalf("unexpected type %s", env.Type) } } func TestServerProvisionPayload_JSONRoundtrip(t *testing.T) { deadline := time.Date(2026, 6, 26, 12, 0, 0, 0, time.UTC) original := ServerProvisionPayload{ OperationMeta: OperationMeta{ ServerID: "srv-abc123", Generation: 2, Deadline: &deadline, IdempotencyKey: "idem-001", }, Edition: "JAVA", SoftwareFamily: "PAPER", MinecraftVersion: "1.21.1", HostPort: 25565, RamMb: 2048, DataPath: "/var/lib/hexahost-gamecloud/srv-abc123", EulaAccepted: true, } data, err := json.Marshal(original) if err != nil { t.Fatalf("marshal: %v", err) } var decoded ServerProvisionPayload if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("unmarshal: %v", err) } if decoded.ServerID != original.ServerID { t.Fatalf("serverId: got %q want %q", decoded.ServerID, original.ServerID) } if decoded.Generation != original.Generation { t.Fatalf("generation: got %d want %d", decoded.Generation, original.Generation) } if decoded.Edition != original.Edition { t.Fatalf("edition: got %q want %q", decoded.Edition, original.Edition) } if decoded.SoftwareFamily != original.SoftwareFamily { t.Fatalf("softwareFamily: got %q want %q", decoded.SoftwareFamily, original.SoftwareFamily) } if decoded.MinecraftVersion != original.MinecraftVersion { t.Fatalf("minecraftVersion: got %q want %q", decoded.MinecraftVersion, original.MinecraftVersion) } if decoded.HostPort != original.HostPort { t.Fatalf("hostPort: got %d want %d", decoded.HostPort, original.HostPort) } if decoded.RamMb != original.RamMb { t.Fatalf("ramMb: got %d want %d", decoded.RamMb, original.RamMb) } if decoded.DataPath != original.DataPath { t.Fatalf("dataPath: got %q want %q", decoded.DataPath, original.DataPath) } if decoded.EulaAccepted != original.EulaAccepted { t.Fatalf("eulaAccepted: got %v want %v", decoded.EulaAccepted, original.EulaAccepted) } if decoded.IdempotencyKey != original.IdempotencyKey { t.Fatalf("idempotencyKey: got %q want %q", decoded.IdempotencyKey, original.IdempotencyKey) } if decoded.Deadline == nil || !decoded.Deadline.Equal(deadline) { t.Fatalf("deadline mismatch: got %v want %v", decoded.Deadline, deadline) } } func TestNewEnvelope(t *testing.T) { env, err := NewEnvelope(TypeAgentHello, "msg-new", AgentHelloPayload{ NodeID: "n1", AgentVersion: "0.1.0", ProtocolVersion: 1, }) if err != nil { t.Fatalf("new envelope: %v", err) } if env.ProtocolVersion != CurrentProtocolVersion { t.Fatalf("protocol version %d", env.ProtocolVersion) } }