- 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
223 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|