- 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
246 lines
5.8 KiB
Go
246 lines
5.8 KiB
Go
package testenv
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"gcy_hpc_server/internal/model"
|
|
"gcy_hpc_server/internal/storage"
|
|
)
|
|
|
|
func TestNewTestEnv(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
defer func() {
|
|
env.taskSvc.StopProcessor()
|
|
env.poller.Stop()
|
|
env.srv.Close()
|
|
}()
|
|
|
|
if env.DB == nil {
|
|
t.Fatal("DB is nil")
|
|
}
|
|
if env.MockSlurm == nil {
|
|
t.Fatal("MockSlurm is nil")
|
|
}
|
|
if env.MockMinIO == nil {
|
|
t.Fatal("MockMinIO is nil")
|
|
}
|
|
if env.srv == nil {
|
|
t.Fatal("srv is nil")
|
|
}
|
|
|
|
resp, err := http.Get(env.srv.URL + "/api/v1/applications")
|
|
if err != nil {
|
|
t.Fatalf("GET /api/v1/applications: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestAllRoutesRegistered(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
|
|
routes := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{"POST", "/api/v1/jobs/submit"},
|
|
{"GET", "/api/v1/jobs"},
|
|
{"GET", "/api/v1/jobs/history"},
|
|
{"GET", "/api/v1/jobs/1"},
|
|
{"DELETE", "/api/v1/jobs/1"},
|
|
{"GET", "/api/v1/nodes"},
|
|
{"GET", "/api/v1/nodes/node01"},
|
|
{"GET", "/api/v1/partitions"},
|
|
{"GET", "/api/v1/partitions/normal"},
|
|
{"GET", "/api/v1/diag"},
|
|
{"GET", "/api/v1/applications"},
|
|
{"POST", "/api/v1/applications"},
|
|
{"GET", "/api/v1/applications/1"},
|
|
{"PUT", "/api/v1/applications/1"},
|
|
{"DELETE", "/api/v1/applications/1"},
|
|
{"POST", "/api/v1/applications/1/submit"},
|
|
{"POST", "/api/v1/files/uploads"},
|
|
{"GET", "/api/v1/files/uploads/1"},
|
|
{"PUT", "/api/v1/files/uploads/1/chunks/0"},
|
|
{"POST", "/api/v1/files/uploads/1/complete"},
|
|
{"DELETE", "/api/v1/files/uploads/1"},
|
|
{"GET", "/api/v1/files"},
|
|
{"GET", "/api/v1/files/1"},
|
|
{"GET", "/api/v1/files/1/download"},
|
|
{"DELETE", "/api/v1/files/1"},
|
|
{"POST", "/api/v1/files/folders"},
|
|
{"GET", "/api/v1/files/folders"},
|
|
{"GET", "/api/v1/files/folders/1"},
|
|
{"DELETE", "/api/v1/files/folders/1"},
|
|
{"POST", "/api/v1/tasks"},
|
|
{"GET", "/api/v1/tasks"},
|
|
}
|
|
|
|
if len(routes) != 31 {
|
|
t.Fatalf("expected 31 routes, got %d", len(routes))
|
|
}
|
|
|
|
for _, r := range routes {
|
|
req, err := http.NewRequest(r.method, env.srv.URL+r.path, nil)
|
|
if err != nil {
|
|
t.Fatalf("%s %s: create request: %v", r.method, r.path, err)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("%s %s: %v", r.method, r.path, err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
|
|
// Gin returns 404 for unregistered routes WITHOUT a JSON body.
|
|
// Handler-level 404s return JSON with {"success":false,...}.
|
|
// So a 404 that decodes as valid JSON means the route IS registered.
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
var apiResp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
if json.Unmarshal(body, &apiResp) != nil {
|
|
t.Errorf("%s %s: got router-level 404 (route not registered)", r.method, r.path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMakeTaskStale(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
|
|
ctx := t.Context()
|
|
task := &model.Task{
|
|
TaskName: "stale-test",
|
|
AppID: 1,
|
|
AppName: "test-app",
|
|
Status: model.TaskStatusSubmitted,
|
|
}
|
|
taskID, err := env.taskStore.Create(ctx, task)
|
|
if err != nil {
|
|
t.Fatalf("create task: %v", err)
|
|
}
|
|
|
|
before := time.Now()
|
|
if err := env.MakeTaskStale(taskID); err != nil {
|
|
t.Fatalf("MakeTaskStale: %v", err)
|
|
}
|
|
|
|
updated, err := env.taskStore.GetByID(ctx, taskID)
|
|
if err != nil {
|
|
t.Fatalf("get task: %v", err)
|
|
}
|
|
if !updated.UpdatedAt.Before(before.Add(-25 * time.Second)) {
|
|
t.Errorf("expected updated_at to be >25s in the past, got %v (before=%v)", updated.UpdatedAt, before)
|
|
}
|
|
}
|
|
|
|
func TestUploadTestData(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
|
|
content := []byte("hello testenv")
|
|
fileID, blobID := env.UploadTestData("test.txt", content)
|
|
|
|
if fileID == 0 {
|
|
t.Fatal("fileID is 0")
|
|
}
|
|
if blobID == 0 {
|
|
t.Fatal("blobID is 0")
|
|
}
|
|
|
|
ctx := t.Context()
|
|
file, err := env.fileStore.GetByID(ctx, fileID)
|
|
if err != nil {
|
|
t.Fatalf("get file: %v", err)
|
|
}
|
|
if file == nil {
|
|
t.Fatal("file not found")
|
|
}
|
|
if file.Name != "test.txt" {
|
|
t.Errorf("expected name 'test.txt', got %q", file.Name)
|
|
}
|
|
|
|
blob, err := env.blobStore.GetBySHA256(ctx, file.BlobSHA256)
|
|
if err != nil {
|
|
t.Fatalf("get blob: %v", err)
|
|
}
|
|
if blob == nil {
|
|
t.Fatal("blob not found")
|
|
}
|
|
if blob.FileSize != int64(len(content)) {
|
|
t.Errorf("expected size %d, got %d", len(content), blob.FileSize)
|
|
}
|
|
|
|
obj, info, err := env.MockMinIO.GetObject(ctx, "files", blob.MinioKey, storage.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("get object: %v", err)
|
|
}
|
|
defer obj.Close()
|
|
got, _ := io.ReadAll(obj)
|
|
if string(got) != string(content) {
|
|
t.Errorf("expected content %q, got %q", content, got)
|
|
}
|
|
if info.Size != int64(len(content)) {
|
|
t.Errorf("expected object size %d, got %d", len(content), info.Size)
|
|
}
|
|
}
|
|
|
|
func TestCreateApp(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
|
|
params := json.RawMessage(`[{"name":"cores","type":"integer","required":true}]`)
|
|
appID, err := env.CreateApp("test-app", "#!/bin/bash\necho hello", params)
|
|
if err != nil {
|
|
t.Fatalf("CreateApp: %v", err)
|
|
}
|
|
if appID == 0 {
|
|
t.Fatal("appID is 0")
|
|
}
|
|
|
|
ctx := t.Context()
|
|
app, err := env.appStore.GetByID(ctx, appID)
|
|
if err != nil {
|
|
t.Fatalf("get app: %v", err)
|
|
}
|
|
if app == nil {
|
|
t.Fatal("app not found")
|
|
}
|
|
if app.Name != "test-app" {
|
|
t.Errorf("expected name 'test-app', got %q", app.Name)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_SetsJSONContentType(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
|
|
resp := env.DoRequest("POST", "/api/v1/applications", nil)
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == 0 {
|
|
t.Fatal("no status code")
|
|
}
|
|
}
|
|
|
|
func TestDecodeResponse(t *testing.T) {
|
|
env := NewTestEnv(t)
|
|
|
|
resp := env.DoRequest("GET", "/api/v1/applications", nil)
|
|
defer resp.Body.Close()
|
|
|
|
success, data, err := env.DecodeResponse(resp)
|
|
if err != nil {
|
|
t.Fatalf("DecodeResponse: %v", err)
|
|
}
|
|
if !success {
|
|
t.Error("expected success=true for list applications")
|
|
}
|
|
if data == nil {
|
|
t.Error("expected non-nil data")
|
|
}
|
|
}
|