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:
186
cmd/server/integration_cluster_test.go
Normal file
186
cmd/server/integration_cluster_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gcy_hpc_server/internal/testutil/testenv"
|
||||
)
|
||||
|
||||
// clusterNodeData mirrors the NodeResponse DTO returned by the API.
|
||||
type clusterNodeData struct {
|
||||
Name string `json:"name"`
|
||||
State []string `json:"state"`
|
||||
CPUs int32 `json:"cpus"`
|
||||
RealMemory int64 `json:"real_memory"`
|
||||
}
|
||||
|
||||
// clusterPartitionData mirrors the PartitionResponse DTO returned by the API.
|
||||
type clusterPartitionData struct {
|
||||
Name string `json:"name"`
|
||||
State []string `json:"state"`
|
||||
TotalNodes int32 `json:"total_nodes,omitempty"`
|
||||
TotalCPUs int32 `json:"total_cpus,omitempty"`
|
||||
}
|
||||
|
||||
// clusterDiagStat mirrors a single entry from the diag statistics.
|
||||
type clusterDiagStat struct {
|
||||
Parts []struct {
|
||||
Param string `json:"param"`
|
||||
} `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// clusterDecodeAll decodes the response and returns status, success, and raw data.
|
||||
func clusterDecodeAll(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
|
||||
}
|
||||
|
||||
// TestIntegration_Cluster_Nodes verifies GET /api/v1/nodes returns the 3 pre-loaded mock nodes.
|
||||
func TestIntegration_Cluster_Nodes(t *testing.T) {
|
||||
env := testenv.NewTestEnv(t)
|
||||
|
||||
resp := env.DoRequest(http.MethodGet, "/api/v1/nodes", nil)
|
||||
status, success, data, err := clusterDecodeAll(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 nodes []clusterNodeData
|
||||
if err := json.Unmarshal(data, &nodes); err != nil {
|
||||
t.Fatalf("unmarshal nodes: %v", err)
|
||||
}
|
||||
if len(nodes) != 3 {
|
||||
t.Fatalf("expected 3 nodes, got %d", len(nodes))
|
||||
}
|
||||
|
||||
names := make(map[string]bool, len(nodes))
|
||||
for _, n := range nodes {
|
||||
names[n.Name] = true
|
||||
}
|
||||
for _, expected := range []string{"node01", "node02", "node03"} {
|
||||
if !names[expected] {
|
||||
t.Errorf("missing expected node %q", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_Cluster_NodeByName verifies GET /api/v1/nodes/:name returns a single node.
|
||||
func TestIntegration_Cluster_NodeByName(t *testing.T) {
|
||||
env := testenv.NewTestEnv(t)
|
||||
|
||||
resp := env.DoRequest(http.MethodGet, "/api/v1/nodes/node01", nil)
|
||||
status, success, data, err := clusterDecodeAll(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 node clusterNodeData
|
||||
if err := json.Unmarshal(data, &node); err != nil {
|
||||
t.Fatalf("unmarshal node: %v", err)
|
||||
}
|
||||
if node.Name != "node01" {
|
||||
t.Fatalf("expected name=node01, got %q", node.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_Cluster_Partitions verifies GET /api/v1/partitions returns the 2 pre-loaded partitions.
|
||||
func TestIntegration_Cluster_Partitions(t *testing.T) {
|
||||
env := testenv.NewTestEnv(t)
|
||||
|
||||
resp := env.DoRequest(http.MethodGet, "/api/v1/partitions", nil)
|
||||
status, success, data, err := clusterDecodeAll(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 partitions []clusterPartitionData
|
||||
if err := json.Unmarshal(data, &partitions); err != nil {
|
||||
t.Fatalf("unmarshal partitions: %v", err)
|
||||
}
|
||||
if len(partitions) != 2 {
|
||||
t.Fatalf("expected 2 partitions, got %d", len(partitions))
|
||||
}
|
||||
|
||||
names := make(map[string]bool, len(partitions))
|
||||
for _, p := range partitions {
|
||||
names[p.Name] = true
|
||||
}
|
||||
if !names["normal"] {
|
||||
t.Error("missing expected partition \"normal\"")
|
||||
}
|
||||
if !names["gpu"] {
|
||||
t.Error("missing expected partition \"gpu\"")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_Cluster_PartitionByName verifies GET /api/v1/partitions/:name returns a single partition.
|
||||
func TestIntegration_Cluster_PartitionByName(t *testing.T) {
|
||||
env := testenv.NewTestEnv(t)
|
||||
|
||||
resp := env.DoRequest(http.MethodGet, "/api/v1/partitions/normal", nil)
|
||||
status, success, data, err := clusterDecodeAll(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 part clusterPartitionData
|
||||
if err := json.Unmarshal(data, &part); err != nil {
|
||||
t.Fatalf("unmarshal partition: %v", err)
|
||||
}
|
||||
if part.Name != "normal" {
|
||||
t.Fatalf("expected name=normal, got %q", part.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_Cluster_Diag verifies GET /api/v1/diag returns diagnostics data.
|
||||
func TestIntegration_Cluster_Diag(t *testing.T) {
|
||||
env := testenv.NewTestEnv(t)
|
||||
|
||||
resp := env.DoRequest(http.MethodGet, "/api/v1/diag", nil)
|
||||
status, success, data, err := clusterDecodeAll(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")
|
||||
}
|
||||
|
||||
// Verify the response contains a "statistics" field (non-empty JSON object).
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatalf("unmarshal diag top-level: %v", err)
|
||||
}
|
||||
if _, ok := raw["statistics"]; !ok {
|
||||
t.Fatal("diag response missing \"statistics\" field")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user