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") } }