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" "git.eeqj.de/sneak/upaas/internal/models" ) func setupAPITest(t *testing.T) (*testContext, string) { t.Helper() tc := setupTestHandlers(t) // Create a user first. _, err := tc.authSvc.CreateUser(t.Context(), "admin", "password123") require.NoError(t, err) user, err := models.FindUserByUsername(t.Context(), tc.database, "admin") require.NoError(t, err) require.NotNil(t, user) // Generate an API token. rawToken, _, err := models.GenerateAPIToken(t.Context(), tc.database, user.ID, "test") require.NoError(t, err) return tc, rawToken } func apiRequest( t *testing.T, tc *testContext, token, 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) } req.Header.Set("Authorization", "Bearer "+token) rr := httptest.NewRecorder() // Build a chi router with API routes. r := chi.NewRouter() mw := tc.middleware r.Route("/api/v1", func(apiR chi.Router) { apiR.Use(mw.APITokenAuth()) apiR.Get("/whoami", tc.handlers.HandleAPIWhoAmI()) apiR.Post("/tokens", tc.handlers.HandleAPICreateToken()) 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()) }) r.ServeHTTP(rr, req) return rr } func TestAPIAuthRejectsNoToken(t *testing.T) { t.Parallel() tc := setupTestHandlers(t) req := httptest.NewRequest(http.MethodGet, "/api/v1/apps", nil) rr := httptest.NewRecorder() r := chi.NewRouter() r.Route("/api/v1", func(apiR chi.Router) { apiR.Use(tc.middleware.APITokenAuth()) apiR.Get("/apps", tc.handlers.HandleAPIListApps()) }) r.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) } func TestAPIAuthRejectsInvalidToken(t *testing.T) { t.Parallel() tc := setupTestHandlers(t) rr := apiRequest(t, tc, "invalid-token", http.MethodGet, "/api/v1/apps", "") assert.Equal(t, http.StatusUnauthorized, rr.Code) } func TestAPIWhoAmI(t *testing.T) { t.Parallel() tc, token := setupAPITest(t) rr := apiRequest(t, tc, token, 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, token := setupAPITest(t) rr := apiRequest(t, tc, token, 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, token := setupAPITest(t) body := `{"name":"test-app","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, token, 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, token := setupAPITest(t) // Missing required fields. body := `{"name":"","repoUrl":""}` rr := apiRequest(t, tc, token, http.MethodPost, "/api/v1/apps", body) assert.Equal(t, http.StatusBadRequest, rr.Code) } func TestAPIGetApp(t *testing.T) { t.Parallel() tc, token := setupAPITest(t) // Create an app first. body := `{"name":"my-app","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, token, 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) // Get the app. rr = apiRequest(t, tc, token, 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, token := setupAPITest(t) rr := apiRequest(t, tc, token, http.MethodGet, "/api/v1/apps/nonexistent", "") assert.Equal(t, http.StatusNotFound, rr.Code) } func TestAPIDeleteApp(t *testing.T) { t.Parallel() tc, token := setupAPITest(t) // Create an app. body := `{"name":"delete-me","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, token, 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) // Delete it. rr = apiRequest(t, tc, token, http.MethodDelete, "/api/v1/apps/"+appID, "") assert.Equal(t, http.StatusOK, rr.Code) // Verify it's gone. rr = apiRequest(t, tc, token, http.MethodGet, "/api/v1/apps/"+appID, "") assert.Equal(t, http.StatusNotFound, rr.Code) } func TestAPIListDeployments(t *testing.T) { t.Parallel() tc, token := setupAPITest(t) // Create an app. body := `{"name":"deploy-app","repoUrl":"https://github.com/example/repo"}` rr := apiRequest(t, tc, token, 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) // List deployments (should be empty). rr = apiRequest(t, tc, token, 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) } func TestAPICreateToken(t *testing.T) { t.Parallel() tc, token := setupAPITest(t) body := `{"name":"new-token"}` rr := apiRequest(t, tc, token, http.MethodPost, "/api/v1/tokens", body) assert.Equal(t, http.StatusCreated, rr.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp)) assert.Equal(t, "new-token", resp["name"]) assert.NotEmpty(t, resp["token"]) }