package api import ( "encoding/json" "testing" "time" ) func TestEnvelopeRoundTrip(t *testing.T) { t.Parallel() hello := HelloPayload{ ProtocolVersion: CurrentProtocolVersion, AgentVersion: "0.1.0", ResticVersion: "0.17.1", Hostname: "test-host", OS: OSLinux, Arch: ArchAmd64, } env, err := Marshal(MsgHello, "", hello) if err != nil { t.Fatalf("marshal: %v", err) } wire, err := json.Marshal(env) if err != nil { t.Fatalf("marshal envelope: %v", err) } var decoded Envelope if err := json.Unmarshal(wire, &decoded); err != nil { t.Fatalf("unmarshal envelope: %v", err) } if decoded.Type != MsgHello { t.Errorf("type: got %q want %q", decoded.Type, MsgHello) } var got HelloPayload if err := decoded.UnmarshalPayload(&got); err != nil { t.Fatalf("unmarshal payload: %v", err) } if got != hello { t.Errorf("round-trip mismatch: %+v != %+v", got, hello) } } func TestEnvelopeNilPayload(t *testing.T) { t.Parallel() env, err := Marshal(MsgHeartbeat, "", nil) if err != nil { t.Fatalf("marshal: %v", err) } if len(env.Payload) != 0 { t.Errorf("nil payload should encode as empty, got %q", env.Payload) } // Unmarshalling nothing into anything must not error. var hb HeartbeatPayload if err := env.UnmarshalPayload(&hb); err != nil { t.Errorf("unmarshal empty payload: %v", err) } } func TestEnvelopeRPCCorrelation(t *testing.T) { t.Parallel() cmd := CommandRunPayload{JobID: "01HJ8K7", Kind: JobBackup} env, err := Marshal(MsgCommandRun, "req-1", cmd) if err != nil { t.Fatalf("marshal: %v", err) } if env.ID != "req-1" { t.Errorf("id not preserved: %q", env.ID) } res := CommandResultPayload{JobID: "01HJ8K7", Accepted: true} resEnv, err := Marshal(MsgCommandResult, env.ID, res) if err != nil { t.Fatalf("marshal result: %v", err) } if resEnv.ID != env.ID { t.Errorf("rpc id mismatch: req=%q res=%q", env.ID, resEnv.ID) } } func TestErrorPayload(t *testing.T) { t.Parallel() ep := ErrorPayload{ Code: ErrProtocolTooOld, Message: "agent protocol_version 0 below minimum 1", HelpURL: "https://example.com/upgrade", } env, err := Marshal(MsgError, "", ep) if err != nil { t.Fatalf("marshal: %v", err) } wire, _ := json.Marshal(env) var decoded Envelope if err := json.Unmarshal(wire, &decoded); err != nil { t.Fatalf("unmarshal: %v", err) } var got ErrorPayload if err := decoded.UnmarshalPayload(&got); err != nil { t.Fatalf("unmarshal payload: %v", err) } if got.Code != ErrProtocolTooOld { t.Errorf("code: got %q want %q", got.Code, ErrProtocolTooOld) } } func TestProtocolVersionConstants(t *testing.T) { t.Parallel() if CurrentProtocolVersion < 1 { t.Errorf("CurrentProtocolVersion must be >= 1, got %d", CurrentProtocolVersion) } if MinAgentProtocolVersion > CurrentProtocolVersion { t.Errorf("min %d > current %d — server would refuse all agents", MinAgentProtocolVersion, CurrentProtocolVersion) } } func TestJobProgressShapeStable(t *testing.T) { t.Parallel() // Locks the JSON field names from spec.md §6.2 so a rename here // breaks tests instead of silently breaking the agent. p := JobProgressPayload{ JobID: "j", PercentDone: 0.5, FilesDone: 1, TotalFiles: 2, BytesDone: 100, TotalBytes: 200, ETASeconds: 30, ThroughputBps: 1000, } raw, _ := json.Marshal(p) want := `{"job_id":"j","percent_done":0.5,"files_done":1,"total_files":2,"bytes_done":100,"total_bytes":200,"eta_seconds":30,"throughput_bps":1000}` if string(raw) != want { t.Errorf("wire shape drifted:\n got %s\n want %s", raw, want) } } // touch time so the import is used by other tests in this file when // they grow over time. var _ = time.Now