package main import ( "bytes" "encoding/json" "fmt" "net/http" "testing" "gcy_hpc_server/internal/testutil/testenv" ) // jobItemData mirrors the JobResponse DTO for a single job. type jobItemData struct { JobID int32 `json:"job_id"` Name string `json:"name"` State []string `json:"job_state"` Partition string `json:"partition"` } // jobListData mirrors the paginated JobListResponse DTO. type jobListData struct { Jobs []jobItemData `json:"jobs"` Total int `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` } // jobCancelData mirrors the cancel response message. type jobCancelData struct { Message string `json:"message"` } // jobDecodeAll decodes the response and returns status, success, and raw data. func jobDecodeAll(env *testenv.TestEnv, resp *http.Response) (statusCode int, success bool, data json.RawMessage, err error) { statusCode = resp.StatusCode success, data, err = env.DecodeResponse(resp) return } // jobSubmitBody builds a JSON body for job submit requests. func jobSubmitBody(script string) *bytes.Reader { body, _ := json.Marshal(map[string]string{"script": script}) return bytes.NewReader(body) } // jobSubmitViaAPI submits a job and returns the job ID. Fatals on failure. func jobSubmitViaAPI(t *testing.T, env *testenv.TestEnv, script string) int32 { t.Helper() resp := env.DoRequest(http.MethodPost, "/api/v1/jobs/submit", jobSubmitBody(script)) status, success, data, err := jobDecodeAll(env, resp) if err != nil { t.Fatalf("submit job decode: %v", err) } if status != http.StatusCreated { t.Fatalf("expected status 201, got %d", status) } if !success { t.Fatal("expected success=true on submit") } var job jobItemData if err := json.Unmarshal(data, &job); err != nil { t.Fatalf("unmarshal submitted job: %v", err) } return job.JobID } // TestIntegration_Jobs_Submit verifies POST /api/v1/jobs/submit creates a new job. func TestIntegration_Jobs_Submit(t *testing.T) { env := testenv.NewTestEnv(t) script := "#!/bin/bash\necho hello" resp := env.DoRequest(http.MethodPost, "/api/v1/jobs/submit", jobSubmitBody(script)) status, success, data, err := jobDecodeAll(env, resp) if err != nil { t.Fatalf("decode response: %v", err) } if status != http.StatusCreated { t.Fatalf("expected status 201, got %d", status) } if !success { t.Fatal("expected success=true") } var job jobItemData if err := json.Unmarshal(data, &job); err != nil { t.Fatalf("unmarshal job: %v", err) } if job.JobID <= 0 { t.Fatalf("expected positive job_id, got %d", job.JobID) } } // TestIntegration_Jobs_List verifies GET /api/v1/jobs returns a paginated job list. func TestIntegration_Jobs_List(t *testing.T) { env := testenv.NewTestEnv(t) // Submit a job so the list is not empty. jobSubmitViaAPI(t, env, "#!/bin/bash\necho list-test") resp := env.DoRequest(http.MethodGet, "/api/v1/jobs", nil) status, success, data, err := jobDecodeAll(env, resp) if err != nil { t.Fatalf("decode response: %v", err) } if status != http.StatusOK { t.Fatalf("expected status 200, got %d", status) } if !success { t.Fatal("expected success=true") } var list jobListData if err := json.Unmarshal(data, &list); err != nil { t.Fatalf("unmarshal job list: %v", err) } if list.Total < 1 { t.Fatalf("expected at least 1 job, got total=%d", list.Total) } if list.Page != 1 { t.Fatalf("expected page=1, got %d", list.Page) } } // TestIntegration_Jobs_Get verifies GET /api/v1/jobs/:id returns a single job. func TestIntegration_Jobs_Get(t *testing.T) { env := testenv.NewTestEnv(t) jobID := jobSubmitViaAPI(t, env, "#!/bin/bash\necho get-test") path := fmt.Sprintf("/api/v1/jobs/%d", jobID) resp := env.DoRequest(http.MethodGet, path, nil) status, success, data, err := jobDecodeAll(env, resp) if err != nil { t.Fatalf("decode response: %v", err) } if status != http.StatusOK { t.Fatalf("expected status 200, got %d", status) } if !success { t.Fatal("expected success=true") } var job jobItemData if err := json.Unmarshal(data, &job); err != nil { t.Fatalf("unmarshal job: %v", err) } if job.JobID != jobID { t.Fatalf("expected job_id=%d, got %d", jobID, job.JobID) } } // TestIntegration_Jobs_Cancel verifies DELETE /api/v1/jobs/:id cancels a job. func TestIntegration_Jobs_Cancel(t *testing.T) { env := testenv.NewTestEnv(t) jobID := jobSubmitViaAPI(t, env, "#!/bin/bash\necho cancel-test") path := fmt.Sprintf("/api/v1/jobs/%d", jobID) resp := env.DoRequest(http.MethodDelete, path, nil) status, success, data, err := jobDecodeAll(env, resp) if err != nil { t.Fatalf("decode response: %v", err) } if status != http.StatusOK { t.Fatalf("expected status 200, got %d", status) } if !success { t.Fatal("expected success=true") } var msg jobCancelData if err := json.Unmarshal(data, &msg); err != nil { t.Fatalf("unmarshal cancel response: %v", err) } if msg.Message == "" { t.Fatal("expected non-empty cancel message") } } // TestIntegration_Jobs_History verifies GET /api/v1/jobs/history returns historical jobs. func TestIntegration_Jobs_History(t *testing.T) { env := testenv.NewTestEnv(t) // Submit and cancel a job so it moves from active to history queue. jobID := jobSubmitViaAPI(t, env, "#!/bin/bash\necho history-test") path := fmt.Sprintf("/api/v1/jobs/%d", jobID) env.DoRequest(http.MethodDelete, path, nil) resp := env.DoRequest(http.MethodGet, "/api/v1/jobs/history", nil) status, success, data, err := jobDecodeAll(env, resp) if err != nil { t.Fatalf("decode response: %v", err) } if status != http.StatusOK { t.Fatalf("expected status 200, got %d", status) } if !success { t.Fatal("expected success=true") } var list jobListData if err := json.Unmarshal(data, &list); err != nil { t.Fatalf("unmarshal history: %v", err) } if list.Total < 1 { t.Fatalf("expected at least 1 history job, got total=%d", list.Total) } // Verify the cancelled job appears in history. found := false for _, j := range list.Jobs { if j.JobID == jobID { found = true break } } if !found { t.Fatalf("cancelled job %d not found in history", jobID) } }