Files
browser/platform/api/control_handlers_test.go

136 lines
6.5 KiB
Go

package api
import (
"net/http"
"net/http/httptest"
"testing"
"browser.local/platform/domain"
"browser.local/platform/dto"
)
func TestRunControlAPIHelloHeartbeatWorkflow(t *testing.T) {
router := newTestRouter()
helloRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/hello", validRunControlHelloRequest())
assertStatus(t, helloRecorder, http.StatusOK)
hello := decodeBody[dto.RunControlHelloResponse](t, helloRecorder)
if !hello.Accepted || hello.SessionToken == "" || hello.RunEndpointID != "run-local" {
t.Fatalf("expected accepted hello response, got %+v", hello)
}
endpoint := getJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints/run-local")
if endpoint.Status != domain.RunEndpointStatusOnline || len(endpoint.Capabilities) != 2 {
t.Fatalf("expected registered endpoint metadata, got %+v", endpoint)
}
heartbeatRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
Version: "0.1.1",
Status: domain.RunEndpointStatusOnline,
CapabilityFingerprint: "cap-v2",
Capacity: dto.RunCapacityResponse{
MaxJobs: 4,
RunningJobs: 1,
},
})
assertStatus(t, heartbeatRecorder, http.StatusOK)
heartbeat := decodeBody[dto.RunControlHeartbeatResponse](t, heartbeatRecorder)
if !heartbeat.Accepted || !heartbeat.RefreshCapabilities || heartbeat.NextHeartbeatSeconds <= 0 {
t.Fatalf("expected accepted heartbeat with refresh, got %+v", heartbeat)
}
}
func TestRunLifecycleReportAPIProjectsServerState(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
helloRequest := validRunControlHelloRequest()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "logs.read")
helloRequest.CapabilityReport.Fingerprint = "cap-lifecycle-report"
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, helloRequest))
server := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-lifecycle-report-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Lifecycle Report API", State: domain.ServerInstanceStateReady}, adminSession)
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/lifecycle/report", dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, ServerInstanceID: server.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100, Message: "autonomous start complete"}, Message: "autonomous start complete", ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running", Summary: "private supervised process identity"}})
assertStatus(t, recorder, http.StatusOK)
response := decodeBody[dto.RunLifecycleReportResponse](t, recorder)
if !response.Accepted || response.ProjectedState != domain.ServerInstanceStateRunning {
t.Fatalf("expected lifecycle report projection, got %+v", response)
}
updated := getJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances/"+server.ID, adminSession)
if updated.State != domain.ServerInstanceStateRunning {
t.Fatalf("expected server state projected running, got %+v", updated)
}
}
func TestRunControlAPIReRegistrationRotatesToken(t *testing.T) {
router := newTestRouter()
first := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunControlHelloRequest()))
request := validRunControlHelloRequest()
request.Version = "0.2.0"
second := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, request))
if second.SessionToken == first.SessionToken {
t.Fatalf("expected new session token after re-registration, got %q", second.SessionToken)
}
}
func TestRunControlAPIErrors(t *testing.T) {
router := newTestRouter()
invalid := validRunControlHelloRequest()
invalid.RegistrationToken = ""
invalid.Capacity.RunningJobs = 8
invalid.Capacity.MaxJobs = 4
invalidHello := performJSON(t, router, http.MethodPost, "/api/v1/run/control/hello", invalid)
assertErrorResponse(t, invalidHello, http.StatusBadRequest, errorCodeValidation)
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunControlHelloRequest()))
invalidHeartbeat := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{
RunEndpointID: "run-local",
SessionToken: "stale-token",
Version: "0.1.1",
Status: domain.RunEndpointStatusOnline,
CapabilityFingerprint: "cap-v1",
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
})
assertErrorResponse(t, invalidHeartbeat, http.StatusBadRequest, errorCodeValidation)
validHeartbeat := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
Version: "0.1.1",
Status: domain.RunEndpointStatusOnline,
CapabilityFingerprint: "cap-v1",
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
})
assertStatus(t, validHeartbeat, http.StatusOK)
methodFailure := performRaw(t, router, http.MethodGet, "/api/v1/run/control/hello", "")
assertErrorResponse(t, methodFailure, http.StatusMethodNotAllowed, errorCodeMethodNotAllowed)
}
func performRunControlHello(t *testing.T, router http.Handler, request dto.RunControlHelloRequest) *httptest.ResponseRecorder {
t.Helper()
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/hello", request)
assertStatus(t, recorder, http.StatusOK)
return recorder
}
func validRunControlHelloRequest() dto.RunControlHelloRequest {
return dto.RunControlHelloRequest{
RegistrationToken: "registration-token",
RunEndpointID: "run-local",
DisplayName: "Local Run",
Version: "0.1.0",
Status: domain.RunEndpointStatusOnline,
Platform: "darwin/arm64",
CapabilityReport: dto.RunCapabilityReport{
Capabilities: []string{"control.hello", "control.heartbeat"},
Fingerprint: "cap-v1",
},
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
}
}