- 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
258 lines
7.4 KiB
Go
258 lines
7.4 KiB
Go
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")
|
|
}
|
|
}
|