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
This commit is contained in:
222
cmd/server/integration_job_test.go
Normal file
222
cmd/server/integration_job_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user