Files
browser/platform/api/job_channel_handlers_test.go

162 lines
6.7 KiB
Go

package api
import (
"net/http"
"testing"
"browser.local/platform/domain"
"browser.local/platform/dto"
)
func TestRunJobChannelAPIWorkflow(t *testing.T) {
router := newTestRouter()
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunJobControlHelloRequest()))
heartbeatRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
Version: "0.1.0",
Status: domain.RunEndpointStatusOnline,
CapabilityFingerprint: "cap-jobs",
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
})
assertStatus(t, heartbeatRecorder, http.StatusOK)
postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{
ID: "job-1",
RunEndpointID: "run-local",
Capability: "process.start",
IdempotencyKey: "idem-1",
})
claimRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/claim", dto.RunJobClaimRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
Capabilities: []string{"process.start"},
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
})
assertStatus(t, claimRecorder, http.StatusOK)
claim := decodeBody[dto.RunJobClaimResponse](t, claimRecorder)
if !claim.HasJob || claim.Job.JobID != "job-1" || claim.Job.State != domain.JobStateAccepted {
t.Fatalf("expected claimed accepted job, got %+v", claim)
}
ack := postRunJobAck(t, router, dto.RunJobAckRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
Message: "started",
})
if ack.Job.State != domain.JobStateRunning {
t.Fatalf("expected running ack, got %+v", ack)
}
progressRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/progress", dto.RunJobProgressRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
Progress: dto.JobProgressBody{Percent: 60, Message: "working"},
})
assertStatus(t, progressRecorder, http.StatusOK)
progress := decodeBody[dto.RunJobProgressResponse](t, progressRecorder)
if progress.Job.Progress.Percent != 60 {
t.Fatalf("expected progress update, got %+v", progress)
}
cancelRecorder := performJSON(t, router, http.MethodPost, "/api/v1/jobs/job-1/cancel", dto.RunJobCancelRequestBody{Reason: "operator requested"})
assertStatus(t, cancelRecorder, http.StatusOK)
cancel := decodeBody[dto.RunJobCancelRequestResponse](t, cancelRecorder)
if !cancel.Accepted || cancel.Reason != "operator requested" {
t.Fatalf("expected cancel request, got %+v", cancel)
}
pollRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/cancel", dto.RunJobCancelPollRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
})
assertStatus(t, pollRecorder, http.StatusOK)
poll := decodeBody[dto.RunJobCancelPollResponse](t, pollRecorder)
if !poll.HasCancel || poll.JobID != "job-1" {
t.Fatalf("expected cancel poll result, got %+v", poll)
}
resultRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/result", dto.RunJobResultRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
State: domain.JobStateCancelled,
Progress: dto.JobProgressBody{Percent: 100, Message: "cancelled"},
Message: "cancelled",
})
assertStatus(t, resultRecorder, http.StatusOK)
result := decodeBody[dto.RunJobResultResponse](t, resultRecorder)
if result.Job.State != domain.JobStateCancelled {
t.Fatalf("expected cancelled result, got %+v", result)
}
reconcileRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/reconcile", dto.RunJobReconcileRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
ActiveJobs: []dto.RunJobReconcileEntry{{JobID: "local-only", LeaseToken: "local-lease", Attempt: 1}},
})
assertStatus(t, reconcileRecorder, http.StatusOK)
reconcile := decodeBody[dto.RunJobReconcileResponse](t, reconcileRecorder)
if len(reconcile.ConfirmedJobs) != 0 || len(reconcile.DiscardJobIDs) != 1 || reconcile.DiscardJobIDs[0] != "local-only" {
t.Fatalf("expected no active platform jobs and one unknown local job, got %+v", reconcile)
}
}
func TestRunJobChannelAPIErrors(t *testing.T) {
router := newTestRouter()
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunJobControlHelloRequest()))
postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-1", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-1"})
invalidClaim := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/claim", dto.RunJobClaimRequest{
RunEndpointID: "run-local",
SessionToken: "stale",
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
})
assertErrorResponse(t, invalidClaim, http.StatusBadRequest, errorCodeValidation)
claim := decodeBody[dto.RunJobClaimResponse](t, performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/claim", dto.RunJobClaimRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
}))
invalidProgress := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/progress", dto.RunJobProgressRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
Progress: dto.JobProgressBody{Percent: 101},
})
assertErrorResponse(t, invalidProgress, http.StatusBadRequest, errorCodeValidation)
badMethod := performRaw(t, router, http.MethodGet, "/api/v1/run/jobs/claim", "")
assertErrorResponse(t, badMethod, http.StatusMethodNotAllowed, errorCodeMethodNotAllowed)
}
func postRunJobAck(t *testing.T, router http.Handler, request dto.RunJobAckRequest) dto.RunJobAckResponse {
t.Helper()
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/ack", request)
assertStatus(t, recorder, http.StatusOK)
return decodeBody[dto.RunJobAckResponse](t, recorder)
}
func validRunJobControlHelloRequest() dto.RunControlHelloRequest {
request := validRunControlHelloRequest()
request.CapabilityReport.Capabilities = append(request.CapabilityReport.Capabilities, "process.start")
request.CapabilityReport.Fingerprint = "cap-jobs"
return request
}