package api import ( "net/http" "testing" "github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model" ) func TestRunnerCallback_Success(t *testing.T) { _, _, rs, _, router := newRunnerTestRouter() rs.mu.Lock() rs.runners["r1"] = &model.Runner{ID: "r1", User: "alice", Status: model.RunnerStatusPodCreating} rs.mu.Unlock() body := `{"status":"runner_registered","forgejo_runner_id":"fj-42"}` rr := doRequest(router, http.MethodPost, "/internal/runners/r1/callback", body, "") if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var runner model.Runner decodeJSON(t, rr, &runner) if runner.Status != model.RunnerStatusRunnerRegistered { t.Errorf("expected status runner_registered, got %s", runner.Status) } if runner.ForgejoRunnerID != "fj-42" { t.Errorf("expected forgejo_runner_id fj-42, got %s", runner.ForgejoRunnerID) } } func TestRunnerCallback_NoAuth(t *testing.T) { _, _, rs, _, router := newRunnerTestRouter() rs.mu.Lock() rs.runners["r1"] = &model.Runner{ID: "r1", User: "alice", Status: model.RunnerStatusPodCreating} rs.mu.Unlock() body := `{"status":"runner_registered"}` rr := doRequest(router, http.MethodPost, "/internal/runners/r1/callback", body, "") if rr.Code != http.StatusOK { t.Fatalf("internal callback should work without auth, got %d: %s", rr.Code, rr.Body.String()) } } func TestRunnerCallback_NotFound(t *testing.T) { _, _, _, _, router := newRunnerTestRouter() body := `{"status":"runner_registered"}` rr := doRequest(router, http.MethodPost, "/internal/runners/nonexistent/callback", body, "") if rr.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d: %s", rr.Code, rr.Body.String()) } } func TestRunnerCallback_InvalidTransition(t *testing.T) { _, _, rs, _, router := newRunnerTestRouter() rs.mu.Lock() rs.runners["r1"] = &model.Runner{ID: "r1", User: "alice", Status: model.RunnerStatusReceived} rs.mu.Unlock() body := `{"status":"completed"}` rr := doRequest(router, http.MethodPost, "/internal/runners/r1/callback", body, "") if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 for invalid transition, got %d: %s", rr.Code, rr.Body.String()) } } func TestRunnerCallback_InvalidBody(t *testing.T) { _, _, rs, _, router := newRunnerTestRouter() rs.mu.Lock() rs.runners["r1"] = &model.Runner{ID: "r1", User: "alice", Status: model.RunnerStatusPodCreating} rs.mu.Unlock() rr := doRequest(router, http.MethodPost, "/internal/runners/r1/callback", "not json", "") if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", rr.Code, rr.Body.String()) } } func TestRunnerCallback_CompletedStatus(t *testing.T) { _, _, rs, _, router := newRunnerTestRouter() rs.mu.Lock() rs.runners["r1"] = &model.Runner{ID: "r1", User: "alice", Status: model.RunnerStatusJobClaimed} rs.mu.Unlock() body := `{"status":"completed"}` rr := doRequest(router, http.MethodPost, "/internal/runners/r1/callback", body, "") if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var runner model.Runner decodeJSON(t, rr, &runner) if runner.Status != model.RunnerStatusCompleted { t.Errorf("expected status completed, got %s", runner.Status) } if runner.CompletedAt == nil { t.Error("expected completed_at to be set") } } func TestRunnerCallback_FailedStatus(t *testing.T) { _, _, rs, _, router := newRunnerTestRouter() rs.mu.Lock() rs.runners["r1"] = &model.Runner{ID: "r1", User: "alice", Status: model.RunnerStatusRunnerRegistered} rs.mu.Unlock() body := `{"status":"failed"}` rr := doRequest(router, http.MethodPost, "/internal/runners/r1/callback", body, "") if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var runner model.Runner decodeJSON(t, rr, &runner) if runner.Status != model.RunnerStatusFailed { t.Errorf("expected status failed, got %s", runner.Status) } }