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:
dailz
2026-04-16 13:23:27 +08:00
parent 73504f9fdb
commit b9b2f0d9b4
16 changed files with 4685 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"gcy_hpc_server/internal/testutil/testenv"
)
// appListData mirrors the list endpoint response data structure.
type appListData struct {
Applications []json.RawMessage `json:"applications"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// appCreatedData mirrors the create endpoint response data structure.
type appCreatedData struct {
ID int64 `json:"id"`
}
// appMessageData mirrors the update/delete endpoint response data structure.
type appMessageData struct {
Message string `json:"message"`
}
// appData mirrors the application model returned by GET.
type appData struct {
ID int64 `json:"id"`
Name string `json:"name"`
ScriptTemplate string `json:"script_template"`
Parameters json.RawMessage `json:"parameters,omitempty"`
}
// appDoRequest is a small wrapper that marshals body and calls env.DoRequest.
func appDoRequest(env *testenv.TestEnv, method, path string, body interface{}) *http.Response {
var r io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
panic(fmt.Sprintf("appDoRequest marshal: %v", err))
}
r = bytes.NewReader(b)
}
return env.DoRequest(method, path, r)
}
// appDecodeAll decodes the response and also reads the HTTP status.
func appDecodeAll(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
}
// appSeedApp creates an app via the service (bypasses HTTP) and returns its ID.
func appSeedApp(env *testenv.TestEnv, name string) int64 {
id, err := env.CreateApp(name, "#!/bin/bash\necho hello", json.RawMessage(`[]`))
if err != nil {
panic(fmt.Sprintf("appSeedApp: %v", err))
}
return id
}
// TestIntegration_App_List verifies GET /api/v1/applications returns an empty list initially.
func TestIntegration_App_List(t *testing.T) {
env := testenv.NewTestEnv(t)
resp := env.DoRequest(http.MethodGet, "/api/v1/applications", nil)
status, success, data, err := appDecodeAll(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 appListData
if err := json.Unmarshal(data, &list); err != nil {
t.Fatalf("unmarshal list data: %v", err)
}
if list.Total != 0 {
t.Fatalf("expected total=0, got %d", list.Total)
}
if len(list.Applications) != 0 {
t.Fatalf("expected 0 applications, got %d", len(list.Applications))
}
}
// TestIntegration_App_Create verifies POST /api/v1/applications creates an application.
func TestIntegration_App_Create(t *testing.T) {
env := testenv.NewTestEnv(t)
body := map[string]interface{}{
"name": "test-app-create",
"script_template": "#!/bin/bash\necho hello",
"parameters": []interface{}{},
}
resp := appDoRequest(env, http.MethodPost, "/api/v1/applications", body)
status, success, data, err := appDecodeAll(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 created appCreatedData
if err := json.Unmarshal(data, &created); err != nil {
t.Fatalf("unmarshal created data: %v", err)
}
if created.ID <= 0 {
t.Fatalf("expected positive id, got %d", created.ID)
}
}
// TestIntegration_App_Get verifies GET /api/v1/applications/:id returns the correct application.
func TestIntegration_App_Get(t *testing.T) {
env := testenv.NewTestEnv(t)
id := appSeedApp(env, "test-app-get")
path := fmt.Sprintf("/api/v1/applications/%d", id)
resp := env.DoRequest(http.MethodGet, path, nil)
status, success, data, err := appDecodeAll(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 app appData
if err := json.Unmarshal(data, &app); err != nil {
t.Fatalf("unmarshal app data: %v", err)
}
if app.ID != id {
t.Fatalf("expected id=%d, got %d", id, app.ID)
}
if app.Name != "test-app-get" {
t.Fatalf("expected name=test-app-get, got %s", app.Name)
}
}
// TestIntegration_App_Update verifies PUT /api/v1/applications/:id updates an application.
func TestIntegration_App_Update(t *testing.T) {
env := testenv.NewTestEnv(t)
id := appSeedApp(env, "test-app-update-before")
newName := "test-app-update-after"
body := map[string]interface{}{
"name": newName,
}
path := fmt.Sprintf("/api/v1/applications/%d", id)
resp := appDoRequest(env, http.MethodPut, path, body)
status, success, data, err := appDecodeAll(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 appMessageData
if err := json.Unmarshal(data, &msg); err != nil {
t.Fatalf("unmarshal message data: %v", err)
}
if msg.Message != "application updated" {
t.Fatalf("expected message 'application updated', got %q", msg.Message)
}
getResp := env.DoRequest(http.MethodGet, path, nil)
_, _, getData, gErr := appDecodeAll(env, getResp)
if gErr != nil {
t.Fatalf("decode get response: %v", gErr)
}
var updated appData
if err := json.Unmarshal(getData, &updated); err != nil {
t.Fatalf("unmarshal updated app: %v", err)
}
if updated.Name != newName {
t.Fatalf("expected updated name=%q, got %q", newName, updated.Name)
}
}
// TestIntegration_App_Delete verifies DELETE /api/v1/applications/:id removes an application.
func TestIntegration_App_Delete(t *testing.T) {
env := testenv.NewTestEnv(t)
id := appSeedApp(env, "test-app-delete")
path := fmt.Sprintf("/api/v1/applications/%d", id)
resp := env.DoRequest(http.MethodDelete, path, nil)
status, success, data, err := appDecodeAll(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 appMessageData
if err := json.Unmarshal(data, &msg); err != nil {
t.Fatalf("unmarshal message data: %v", err)
}
if msg.Message != "application deleted" {
t.Fatalf("expected message 'application deleted', got %q", msg.Message)
}
// Verify deletion returns 404.
getResp := env.DoRequest(http.MethodGet, path, nil)
getStatus, getSuccess, _, _ := appDecodeAll(env, getResp)
if getStatus != http.StatusNotFound {
t.Fatalf("expected status 404 after delete, got %d", getStatus)
}
if getSuccess {
t.Fatal("expected success=false after delete")
}
}
// TestIntegration_App_CreateValidation verifies POST /api/v1/applications with empty name returns error.
func TestIntegration_App_CreateValidation(t *testing.T) {
env := testenv.NewTestEnv(t)
body := map[string]interface{}{
"name": "",
"script_template": "#!/bin/bash\necho hello",
}
resp := appDoRequest(env, http.MethodPost, "/api/v1/applications", body)
status, success, _, err := appDecodeAll(env, resp)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if status != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", status)
}
if success {
t.Fatal("expected success=false for validation error")
}
}