package handlers_test import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // apiRouter builds a chi router with the API routes using session auth middleware. func apiRouter(tc *testContext) http.Handler { r := chi.NewRouter() r.Route("/api/v1", func(apiR chi.Router) { apiR.Post("/login", tc.handlers.HandleAPILoginPOST()) apiR.Group(func(apiR chi.Router) { apiR.Use(tc.middleware.APISessionAuth()) apiR.Get("/whoami", tc.handlers.HandleAPIWhoAmI()) apiR.Get("/apps", tc.handlers.HandleAPIListApps()) apiR.Post("/apps", tc.handlers.HandleAPICreateApp()) apiR.Get("/apps/{id}", tc.handlers.HandleAPIGetApp()) apiR.Delete("/apps/{id}", tc.handlers.HandleAPIDeleteApp()) apiR.Post("/apps/{id}/deploy", tc.handlers.HandleAPITriggerDeploy()) apiR.Get("/apps/{id}/deployments", tc.handlers.HandleAPIListDeployments()) }) }) return r } // setupAPITest creates a test context with a user and returns session cookies. func setupAPITest(t *testing.T) (*testContext, []*http.Cookie) { t.Helper() tc := setupTestHandlers(t) // Create a user. _, err := tc.authSvc.CreateUser(t.Context(), "admin", "password123") require.NoError(t, err) // Login via the API to get session cookies. r := apiRouter(tc) loginBody := `{"username":"admin","password":"password123"}` req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(loginBody)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) cookies := rr.Result().Cookies() require.NotEmpty(t, cookies, "login should return session cookies") return tc, cookies } // apiRequest makes an authenticated API request using session cookies. func apiRequest( t *testing.T, tc *testContext, cookies []*http.Cookie, method, path string, body string, ) *httptest.ResponseRecorder { t.Helper() var req *http.Request if body != "" { req = httptest.NewRequest(method, path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } for _, c := range cookies { req.AddCookie(c) } rr := httptest.NewRecorder() r := apiRouter(tc) r.ServeHTTP(rr, req) return rr } func TestAPILoginSuccess(t *testing.T) { t.Parallel() tc := setupTestHandlers(t) _, err := tc.authSvc.CreateUser(t.Context(), "admin", "password123") require.NoError(t, err) r := apiRouter(tc) body := `{"username":"admin","password":"password123"}` req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp)) assert.Equal(t, "admin", resp["username"]) // Should have a Set-Cookie header. assert.NotEmpty(t, rr.Result().Cookies()) } func TestAPILoginInvalidCredentials(t *testing.T) { t.Parallel() tc := setupTestHandlers(t) _, err := tc.authSvc.CreateUser(t.Context(), "admin", "password123") require.NoError(t, err) r := apiRouter(tc) body := `{"username":"admin","password":"wrong"}` req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) } func TestAPILoginMissingFields(t *testing.T) { t.Parallel() tc := setupTestHandlers(t) r := apiRouter(tc) body := `{"username":"","password":""}` req := httptest.NewRequest(http.MethodPost, "/api/v1/login", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) } func TestAPIRejectsUnauthenticated(t *testing.T) { t.Parallel() tc := setupTestHandlers(t) r := apiRouter(tc) req := httptest.NewRequest(http.MethodGet, "/api/v1/apps", nil) rr := httptest.NewRecorder() r.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) } func TestAPIWhoAmI(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) rr := apiRequest(t, tc, cookies, http.MethodGet, "/api/v1/whoami", "") assert.Equal(t, http.StatusOK, rr.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp)) assert.Equal(t, "admin", resp["username"]) } func TestAPIListAppsEmpty(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) rr := apiRequest(t, tc, cookies, http.MethodGet, "/api/v1/apps", "") assert.Equal(t, http.StatusOK, rr.Code) var apps []any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &apps)) assert.Empty(t, apps) } func TestAPICreateApp(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) body := `{"name":"test-app","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps", body) assert.Equal(t, http.StatusCreated, rr.Code) var app map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &app)) assert.Equal(t, "test-app", app["name"]) assert.Equal(t, "pending", app["status"]) } func TestAPICreateAppValidation(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) body := `{"name":"","repoUrl":""}` rr := apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps", body) assert.Equal(t, http.StatusBadRequest, rr.Code) } func TestAPIGetApp(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) body := `{"name":"my-app","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps", body) require.Equal(t, http.StatusCreated, rr.Code) var created map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &created)) appID, ok := created["id"].(string) require.True(t, ok) rr = apiRequest(t, tc, cookies, http.MethodGet, "/api/v1/apps/"+appID, "") assert.Equal(t, http.StatusOK, rr.Code) var app map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &app)) assert.Equal(t, "my-app", app["name"]) } func TestAPIGetAppNotFound(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) rr := apiRequest(t, tc, cookies, http.MethodGet, "/api/v1/apps/nonexistent", "") assert.Equal(t, http.StatusNotFound, rr.Code) } func TestAPIDeleteApp(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) body := `{"name":"delete-me","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps", body) require.Equal(t, http.StatusCreated, rr.Code) var created map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &created)) appID, ok := created["id"].(string) require.True(t, ok) rr = apiRequest(t, tc, cookies, http.MethodDelete, "/api/v1/apps/"+appID, "") assert.Equal(t, http.StatusOK, rr.Code) rr = apiRequest(t, tc, cookies, http.MethodGet, "/api/v1/apps/"+appID, "") assert.Equal(t, http.StatusNotFound, rr.Code) } func TestAPIListDeployments(t *testing.T) { t.Parallel() tc, cookies := setupAPITest(t) body := `{"name":"deploy-app","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps", body) require.Equal(t, http.StatusCreated, rr.Code) var created map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &created)) appID, ok := created["id"].(string) require.True(t, ok) rr = apiRequest(t, tc, cookies, http.MethodGet, "/api/v1/apps/"+appID+"/deployments", "") assert.Equal(t, http.StatusOK, rr.Code) var deployments []any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &deployments)) assert.Empty(t, deployments) }