package api import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "browser.local/run/protocol" ) func TestNewPlatformClientNormalizesBaseURL(t *testing.T) { client, err := NewPlatformClient("http://platform.test/") if err != nil { t.Fatalf("unexpected error: %v", err) } if client.BaseURL() != "http://platform.test" { t.Fatalf("expected normalized base URL, got %q", client.BaseURL()) } } func TestNewPlatformClientRequiresAbsoluteURL(t *testing.T) { if _, err := NewPlatformClient("platform.local"); err == nil { t.Fatal("expected error for URL without scheme and host") } } func TestPlatformClientHelloPostsJSONAndDecodesResponse(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/control/hello" { t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { t.Fatalf("expected JSON content type, got %q", contentType) } if r.Header.Get("X-Run-Signature") != "" { t.Fatal("hello must not be signed with a session that does not exist yet") } var request protocol.RunHelloRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode hello request: %v", err) } if request.RunEndpointID != "run-local" || request.CapabilityReport.Fingerprint != "cap-v1" { t.Fatalf("unexpected hello payload: %+v", request) } writeTestJSON(t, w, protocol.RunHelloResponse{ Accepted: true, RunEndpointID: "run-local", SessionToken: "session-token", ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC), HeartbeatIntervalSeconds: 15, }) })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } response, err := client.Hello(context.Background(), validRunHelloRequest()) if err != nil { t.Fatalf("hello: %v", err) } if !response.Accepted || response.SessionToken != "session-token" || response.HeartbeatIntervalSeconds != 15 { t.Fatalf("unexpected hello response: %+v", response) } } func TestPlatformRequestErrorRecognizesLegacyInvalidSession(t *testing.T) { legacy := PlatformRequestError{Status: http.StatusBadRequest, Path: "/api/v1/run/control/heartbeat", Code: "validation_failed", Details: []string{"sessionToken is invalid"}} if !legacy.SessionInvalid() { t.Fatal("expected legacy invalid session response to be recognized") } if message := legacy.Error(); !strings.Contains(message, "status=400") || !strings.Contains(message, "path=/api/v1/run/control/heartbeat") || !strings.Contains(message, "code=validation_failed") || !strings.Contains(message, "sessionToken is invalid") { t.Fatalf("expected request error to expose status, path, code, and details, got %q", message) } if (PlatformRequestError{Status: http.StatusBadRequest, Code: "validation_failed", Details: []string{"another validation failure"}}).SessionInvalid() { t.Fatal("unexpected validation response must not trigger re-registration") } } func TestPlatformRequestErrorRecognizesLegacyLogSessionMetadata(t *testing.T) { err := PlatformRequestError{Status: http.StatusBadRequest, Code: "validation_failed", Details: []string{"logSessionId and sessionStartedAt must be provided together"}} if !err.LogBatchLegacySessionMetadata() { t.Fatal("expected legacy log session metadata response to be recognized") } if (PlatformRequestError{Status: http.StatusBadRequest, Code: "validation_failed", Details: []string{"other validation failure"}}).LogBatchLegacySessionMetadata() { t.Fatal("unexpected validation response must not be recognized") } } func TestPlatformRequestErrorRecognizesLogSessionMetadataMismatch(t *testing.T) { err := PlatformRequestError{Status: http.StatusBadRequest, Code: "validation_failed", Details: []string{"log session metadata must match stream"}} if !err.LogBatchSessionMetadataMismatch() { t.Fatal("expected log session metadata mismatch response to be recognized") } if (PlatformRequestError{Status: http.StatusBadRequest, Code: "validation_failed", Details: []string{"other validation failure"}}).LogBatchSessionMetadataMismatch() { t.Fatal("unexpected validation response must not be recognized") } } func TestPlatformClientSignsRunChannelRequestsWithUniqueNonce(t *testing.T) { nonces := map[string]struct{}{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("read signed body: %v", err) } verifyRunRequestSignature(t, r, body, "run-local", "session-token") nonce := r.Header.Get("X-Run-Nonce") if _, exists := nonces[nonce]; exists { t.Fatalf("reused Run request nonce %q", nonce) } nonces[nonce] = struct{}{} writeTestJSON(t, w, protocol.RunHeartbeatResponse{Accepted: true, RunEndpointID: "run-local", NextHeartbeatSeconds: 15, ServerTime: time.Now().UTC()}) })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } for range 2 { if _, err := client.Heartbeat(context.Background(), validRunHeartbeatRequest("session-token")); err != nil { t.Fatalf("signed heartbeat: %v", err) } } } func verifyRunRequestSignature(t *testing.T, request *http.Request, body []byte, endpoint string, token string) { t.Helper() if request.Header.Get("X-Run-Endpoint") != endpoint { t.Fatalf("unexpected signed endpoint %q", request.Header.Get("X-Run-Endpoint")) } timestamp := request.Header.Get("X-Run-Timestamp") if _, err := strconv.ParseInt(timestamp, 10, 64); err != nil { t.Fatalf("invalid signed timestamp %q", timestamp) } nonce := request.Header.Get("X-Run-Nonce") if decoded, err := hex.DecodeString(nonce); err != nil || len(decoded) != 16 { t.Fatalf("invalid signed nonce %q", nonce) } bodyHash := sha256.Sum256(body) canonical := strings.Join([]string{request.Method, request.URL.Path, timestamp, nonce, hex.EncodeToString(bodyHash[:])}, "\n") mac := hmac.New(sha256.New, []byte(token)) _, _ = mac.Write([]byte(canonical)) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(request.Header.Get("X-Run-Signature"))) { t.Fatalf("invalid Run request signature") } } func TestPlatformClientHeartbeatPostsJSONAndDecodesResponse(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/control/heartbeat" { t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } var request protocol.RunHeartbeatRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode heartbeat request: %v", err) } if request.SessionToken != "session-token" || request.Capacity.RunningJobs != 1 { t.Fatalf("unexpected heartbeat payload: %+v", request) } writeTestJSON(t, w, protocol.RunHeartbeatResponse{ Accepted: true, RunEndpointID: "run-local", NextHeartbeatSeconds: 15, RefreshCapabilities: true, ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC), }) })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } response, err := client.Heartbeat(context.Background(), validRunHeartbeatRequest("session-token")) if err != nil { t.Fatalf("heartbeat: %v", err) } if !response.Accepted || !response.RefreshCapabilities || response.NextHeartbeatSeconds != 15 { t.Fatalf("unexpected heartbeat response: %+v", response) } } func TestPlatformClientGetsOneTimeSourceRCONInputOverSignedRoute(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/jobs/source-rcon-input" { t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("read Source RCON input request: %v", err) } verifyRunRequestSignature(t, r, body, "run-local", "session-token") if strings.Contains(string(body), "command") || strings.Contains(string(body), "password") { t.Fatalf("Source RCON input request exposed command material: %s", body) } var request protocol.SourceRCONExecutionInputRequest if err := json.Unmarshal(body, &request); err != nil { t.Fatalf("decode Source RCON input request: %v", err) } if request.JobID != "job-source-rcon" || request.LeaseToken != "lease-source-rcon" || request.Attempt != 1 { t.Fatalf("unexpected Source RCON input request: %+v", request) } writeTestJSON(t, w, protocol.SourceRCONExecutionInputResponse{JobID: request.JobID, ServerInstanceID: "server-1", RunEndpointID: request.RunEndpointID, Command: "rcon.status"}) })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } response, err := client.GetSourceRCONExecutionInput(context.Background(), protocol.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: "job-source-rcon", LeaseToken: "lease-source-rcon", Attempt: 1}) if err != nil || response.Command != "rcon.status" || response.RunEndpointID != "run-local" { t.Fatalf("unexpected Source RCON input response=%+v err=%v", response, err) } } func TestPlatformClientGetsFencedProtectedRequestOverSignedRoute(t *testing.T) { expiresAt := time.Now().UTC().Add(time.Minute) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/v1/run/jobs/protected-request-input" { t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("read protected input request: %v", err) } verifyRunRequestSignature(t, r, body, "run-local", "session-token") if strings.Contains(string(body), "SELECT") || strings.Contains(string(body), "dsn") || strings.Contains(string(body), "password") { t.Fatalf("protected input request exposed execution material: %s", body) } var request protocol.ProtectedRequestExecutionInputRequest if err := json.Unmarshal(body, &request); err != nil { t.Fatalf("decode protected input request: %v", err) } if request.JobID != "job-protected" || request.LeaseToken != "lease-protected" || request.FencingToken != 9 || request.Attempt != 1 { t.Fatalf("unexpected protected input request: %+v", request) } writeTestJSON(t, w, protocol.ProtectedRequestExecutionInputResponse{JobID: request.JobID, ServerInstanceID: "server-1", RunEndpointID: request.RunEndpointID, FencingToken: request.FencingToken, Authorized: true, ApprovalState: "approved", QueueState: "claimed", ExpiresAt: expiresAt, Kind: "sql", TransportKey: "scum-database", TargetKey: "scum-database", RequestText: "SELECT player_id FROM players"}) })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } response, err := client.GetProtectedRequestExecutionInput(context.Background(), protocol.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: "session-token", JobID: "job-protected", LeaseToken: "lease-protected", Attempt: 1, FencingToken: 9}) if err != nil || response.RequestText == "" || response.FencingToken != 9 || !response.Authorized || response.ApprovalState != "approved" || response.QueueState != "claimed" { t.Fatalf("unexpected protected input response=%+v err=%v", response, err) } } func TestPlatformClientReturnsErrorForPlatformFailure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"code":"validation_failed"}`)) })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } if _, err := client.Hello(context.Background(), validRunHelloRequest()); err == nil { t.Fatal("expected platform error") } else { var requestError PlatformRequestError if !errors.As(err, &requestError) || requestError.Status != http.StatusBadRequest || !strings.Contains(err.Error(), "code=validation_failed") { t.Fatalf("expected typed platform error with diagnostic code, got %v", err) } } } func TestPlatformClientHelloThenHeartbeatFlow(t *testing.T) { var activeSessionToken string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/run/control/hello": var request protocol.RunHelloRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode hello request: %v", err) } if request.RegistrationToken == "" || request.RunEndpointID != "run-local" { t.Fatalf("unexpected hello request: %+v", request) } activeSessionToken = "session-token" writeTestJSON(t, w, protocol.RunHelloResponse{ Accepted: true, RunEndpointID: request.RunEndpointID, SessionToken: activeSessionToken, ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC), HeartbeatIntervalSeconds: 15, }) case "/api/v1/run/control/heartbeat": var request protocol.RunHeartbeatRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode heartbeat request: %v", err) } if request.SessionToken != activeSessionToken { t.Fatalf("heartbeat did not use active session token: %+v", request) } writeTestJSON(t, w, protocol.RunHeartbeatResponse{ Accepted: true, RunEndpointID: request.RunEndpointID, NextHeartbeatSeconds: 15, ServerTime: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC), }) default: t.Fatalf("unexpected request path %s", r.URL.Path) } })) defer server.Close() client, err := NewPlatformClient(server.URL) if err != nil { t.Fatalf("new client: %v", err) } hello, err := client.Hello(context.Background(), validRunHelloRequest()) if err != nil { t.Fatalf("hello: %v", err) } heartbeat, err := client.Heartbeat(context.Background(), validRunHeartbeatRequest(hello.SessionToken)) if err != nil { t.Fatalf("heartbeat: %v", err) } if !hello.Accepted || !heartbeat.Accepted { t.Fatalf("expected accepted hello and heartbeat, got %+v %+v", hello, heartbeat) } } func writeTestJSON(t *testing.T, w http.ResponseWriter, value any) { t.Helper() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(value); err != nil { t.Fatalf("encode response: %v", err) } } func validRunHelloRequest() protocol.RunHelloRequest { return protocol.RunHelloRequest{ RegistrationToken: "registration-token", RunEndpointID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Status: "online", Platform: "darwin/arm64", CapabilityReport: protocol.RunCapabilityReport{ Capabilities: []string{"control.hello", "control.heartbeat"}, Fingerprint: "cap-v1", }, Capacity: protocol.RunCapacityReport{MaxJobs: 4}, } } func validRunHeartbeatRequest(sessionToken string) protocol.RunHeartbeatRequest { return protocol.RunHeartbeatRequest{ RunEndpointID: "run-local", SessionToken: sessionToken, Version: "0.1.0", Status: "online", CapabilityFingerprint: "cap-v1", Capacity: protocol.RunCapacityReport{ MaxJobs: 4, RunningJobs: 1, }, } }