Files
hpc/cmd/server/integration_job_test.go
dailz b9b2f0d9b4 feat(testutil): add MockSlurm, MockMinIO, TestEnv and 37 integration tests
- mockminio: in-memory ObjectStorage with all 11 methods, thread-safe, SHA256 ETag, Range support
- mockslurm: httptest server with 11 Slurm REST API endpoints, job eviction from active to history queue
- testenv: one-line test environment factory (SQLite + MockSlurm + MockMinIO + all stores/services/handlers + httptest server)
- integration tests: 37 tests covering Jobs(5), Cluster(5), App(6), Upload(5), File(4), Folder(4), Task(4), E2E(1)
- no external dependencies, no existing files modified
2026-04-16 13:23:27 +08:00

223 lines
6.1 KiB
Go

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)
}
}