Files
browser/run/api/platform_client_test.go
T
2026-07-11 14:56:10 +08:00

213 lines
7.0 KiB
Go

package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"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)
}
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 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 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")
}
}
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,
},
}
}