Files
HexaHost-GameCloud/apps/node-agent/internal/protocol/messages_test.go
smueller 9b061c3ee7
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 9s
CI / Go — edge-gateway build (push) Successful in 13s
Phase3
2026-06-26 12:17:26 +02:00

380 lines
9.5 KiB
Go

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)
}
}
func TestPhase3Payloads_JSONRoundtrip(t *testing.T) {
meta := OperationMeta{
ServerID: "srv-01",
Generation: 3,
}
command := ServerCommandPayload{
OperationMeta: meta,
Command: "say hello",
}
commandRaw, err := json.Marshal(command)
if err != nil {
t.Fatal(err)
}
var decodedCommand ServerCommandPayload
if err := json.Unmarshal(commandRaw, &decodedCommand); err != nil {
t.Fatal(err)
}
if decodedCommand.Command != command.Command {
t.Fatalf("command: got %q want %q", decodedCommand.Command, command.Command)
}
logsSub := ServerLogsSubscribePayload{
OperationMeta: meta,
TailLines: 100,
Follow: true,
}
logsSubRaw, err := json.Marshal(logsSub)
if err != nil {
t.Fatal(err)
}
var decodedLogsSub ServerLogsSubscribePayload
if err := json.Unmarshal(logsSubRaw, &decodedLogsSub); err != nil {
t.Fatal(err)
}
if decodedLogsSub.TailLines != 100 || !decodedLogsSub.Follow {
t.Fatalf("logs subscribe mismatch: %+v", decodedLogsSub)
}
filesList := ServerFilesListPayload{
OperationMeta: meta,
Path: "world",
}
filesListRaw, err := json.Marshal(filesList)
if err != nil {
t.Fatal(err)
}
env := &Envelope{
ProtocolVersion: CurrentProtocolVersion,
MessageID: "msg-files-list",
Type: TypeServerFilesList,
Timestamp: time.Now().UTC(),
Payload: filesListRaw,
}
if err := env.Validate(); err != nil {
t.Fatalf("files list envelope: %v", err)
}
filesRead := ServerFilesReadPayload{
OperationMeta: meta,
Path: "server.properties",
MaxBytes: 4096,
}
filesReadRaw, err := json.Marshal(filesRead)
if err != nil {
t.Fatal(err)
}
env = &Envelope{
ProtocolVersion: CurrentProtocolVersion,
MessageID: "msg-files-read",
Type: TypeServerFilesRead,
Timestamp: time.Now().UTC(),
Payload: filesReadRaw,
}
if err := env.Validate(); err != nil {
t.Fatalf("files read envelope: %v", err)
}
filesWrite := ServerFilesWritePayload{
OperationMeta: meta,
Path: "ops.txt",
Content: "test",
Create: true,
}
filesWriteRaw, err := json.Marshal(filesWrite)
if err != nil {
t.Fatal(err)
}
env = &Envelope{
ProtocolVersion: CurrentProtocolVersion,
MessageID: "msg-files-write",
Type: TypeServerFilesWrite,
Timestamp: time.Now().UTC(),
Payload: filesWriteRaw,
}
if err := env.Validate(); err != nil {
t.Fatalf("files write envelope: %v", err)
}
logLine := ServerLogPayload{
OperationMeta: meta,
Line: "[Server] Done",
Stream: "stdout",
}
logLineRaw, err := json.Marshal(logLine)
if err != nil {
t.Fatal(err)
}
env = &Envelope{
ProtocolVersion: CurrentProtocolVersion,
MessageID: "msg-log",
Type: TypeServerLog,
Timestamp: time.Now().UTC(),
Payload: logLineRaw,
}
if err := env.Validate(); err != nil {
t.Fatalf("server log envelope: %v", err)
}
}
func TestServerOperationResultPayload_WithResult(t *testing.T) {
listResult, err := json.Marshal(ServerFilesListResultPayload{
Entries: []FileEntry{{
Name: "server.properties",
Path: "server.properties",
IsDir: false,
Size: 128,
ModifiedAt: "2026-06-26T12:00:00Z",
}},
})
if err != nil {
t.Fatal(err)
}
payload := ServerOperationResultPayload{
OperationMeta: OperationMeta{ServerID: "srv-01", Generation: 1},
ResultCode: "ok",
Operation: "files.list",
Result: listResult,
}
raw, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
var decoded ServerOperationResultPayload
if err := json.Unmarshal(raw, &decoded); err != nil {
t.Fatal(err)
}
if decoded.Operation != "files.list" {
t.Fatalf("operation: got %q", decoded.Operation)
}
if len(decoded.Result) == 0 {
t.Fatal("expected result payload")
}
var result ServerFilesListResultPayload
if err := json.Unmarshal(decoded.Result, &result); err != nil {
t.Fatal(err)
}
if len(result.Entries) != 1 || result.Entries[0].Name != "server.properties" {
t.Fatalf("unexpected list result: %+v", result)
}
}