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:
257
cmd/server/integration_app_test.go
Normal file
257
cmd/server/integration_app_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user