From 5633406f5cf67a248f60ed381fb1a54927b6f63c Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 7 Dec 2024 23:09:57 +0100 Subject: [PATCH] Update handler integration tests --- .../admin_handlers_integration_test.go | 76 ++--- .../auth_handlers_integration_test.go | 313 ++++++++++++------ .../file_handlers_integration_test.go | 46 +-- .../handlers/git_handlers_integration_test.go | 26 +- server/internal/handlers/integration_test.go | 115 +++++-- .../user_handlers_integration_test.go | 47 +-- .../workspace_handlers_integration_test.go | 48 +-- 7 files changed, 427 insertions(+), 244 deletions(-) diff --git a/server/internal/handlers/admin_handlers_integration_test.go b/server/internal/handlers/admin_handlers_integration_test.go index 121ea4b..3a4d618 100644 --- a/server/internal/handlers/admin_handlers_integration_test.go +++ b/server/internal/handlers/admin_handlers_integration_test.go @@ -34,8 +34,8 @@ func TestAdminHandlers_Integration(t *testing.T) { t.Run("user management", func(t *testing.T) { t.Run("list users", func(t *testing.T) { - // Test with admin token - rr := h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, h.AdminToken, nil) + // Test with admin session + rr := h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var users []*models.User @@ -47,12 +47,12 @@ func TestAdminHandlers_Integration(t *testing.T) { assert.True(t, containsUser(users, h.AdminUser), "Admin user not found in users list") assert.True(t, containsUser(users, h.RegularUser), "Regular user not found in users list") - // Test with non-admin token - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) - // Test without token - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, "", nil) + // Test without session + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, nil, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) @@ -64,8 +64,8 @@ func TestAdminHandlers_Integration(t *testing.T) { Role: models.RoleEditor, } - // Test with admin token - rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + // Test with admin session + rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var createdUser models.User @@ -77,7 +77,7 @@ func TestAdminHandlers_Integration(t *testing.T) { assert.NotZero(t, createdUser.LastWorkspaceID) // Test duplicate email - rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminSession, nil) assert.Equal(t, http.StatusConflict, rr.Code) // Test invalid request (missing required fields) @@ -85,19 +85,19 @@ func TestAdminHandlers_Integration(t *testing.T) { Email: "invalid@test.com", // Missing password and role } - rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", invalidReq, h.AdminToken, nil) + rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", invalidReq, h.AdminSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) - // Test with non-admin token - rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) t.Run("get user", func(t *testing.T) { path := fmt.Sprintf("/api/v1/admin/users/%d", h.RegularUser.ID) - // Test with admin token - rr := h.makeRequest(t, http.MethodGet, path, nil, h.AdminToken, nil) + // Test with admin session + rr := h.makeRequest(t, http.MethodGet, path, nil, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var user models.User @@ -106,11 +106,11 @@ func TestAdminHandlers_Integration(t *testing.T) { assert.Equal(t, h.RegularUser.ID, user.ID) // Test non-existent user - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users/999999", nil, h.AdminToken, nil) + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users/999999", nil, h.AdminSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) - // Test with non-admin token - rr = h.makeRequest(t, http.MethodGet, path, nil, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodGet, path, nil, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) @@ -121,8 +121,8 @@ func TestAdminHandlers_Integration(t *testing.T) { Role: models.RoleViewer, } - // Test with admin token - rr := h.makeRequest(t, http.MethodPut, path, updateReq, h.AdminToken, nil) + // Test with admin session + rr := h.makeRequest(t, http.MethodPut, path, updateReq, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var updatedUser models.User @@ -131,8 +131,8 @@ func TestAdminHandlers_Integration(t *testing.T) { assert.Equal(t, updateReq.DisplayName, updatedUser.DisplayName) assert.Equal(t, updateReq.Role, updatedUser.Role) - // Test with non-admin token - rr = h.makeRequest(t, http.MethodPut, path, updateReq, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodPut, path, updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) @@ -145,7 +145,7 @@ func TestAdminHandlers_Integration(t *testing.T) { Role: models.RoleEditor, } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var createdUser models.User @@ -156,19 +156,19 @@ func TestAdminHandlers_Integration(t *testing.T) { // Test deleting own account (should fail) adminPath := fmt.Sprintf("/api/v1/admin/users/%d", h.AdminUser.ID) - rr = h.makeRequest(t, http.MethodDelete, adminPath, nil, h.AdminToken, nil) + rr = h.makeRequest(t, http.MethodDelete, adminPath, nil, h.AdminSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) - // Test with admin token - rr = h.makeRequest(t, http.MethodDelete, path, nil, h.AdminToken, nil) + // Test with admin session + rr = h.makeRequest(t, http.MethodDelete, path, nil, h.AdminSession, nil) assert.Equal(t, http.StatusNoContent, rr.Code) // Verify user is deleted - rr = h.makeRequest(t, http.MethodGet, path, nil, h.AdminToken, nil) + rr = h.makeRequest(t, http.MethodGet, path, nil, h.AdminSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) - // Test with non-admin token - rr = h.makeRequest(t, http.MethodDelete, path, nil, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodDelete, path, nil, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) }) @@ -181,11 +181,11 @@ func TestAdminHandlers_Integration(t *testing.T) { Name: "Test Workspace", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) - // Test with admin token - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/workspaces", nil, h.AdminToken, nil) + // Test with admin session + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/workspaces", nil, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var workspaces []*handlers.WorkspaceStats @@ -206,8 +206,8 @@ func TestAdminHandlers_Integration(t *testing.T) { assert.GreaterOrEqual(t, ws.TotalSize, int64(0)) } - // Test with non-admin token - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/workspaces", nil, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/workspaces", nil, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) }) @@ -218,11 +218,11 @@ func TestAdminHandlers_Integration(t *testing.T) { UserID: h.RegularUser.ID, Name: "Stats Test Workspace", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) - // Test with admin token - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/stats", nil, h.AdminToken, nil) + // Test with admin session + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/stats", nil, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var stats handlers.SystemStats @@ -236,8 +236,8 @@ func TestAdminHandlers_Integration(t *testing.T) { assert.GreaterOrEqual(t, stats.TotalFiles, 0) assert.GreaterOrEqual(t, stats.TotalSize, int64(0)) - // Test with non-admin token - rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/stats", nil, h.RegularToken, nil) + // Test with non-admin session + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/stats", nil, h.RegularSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) } diff --git a/server/internal/handlers/auth_handlers_integration_test.go b/server/internal/handlers/auth_handlers_integration_test.go index 6db4153..fa979a6 100644 --- a/server/internal/handlers/auth_handlers_integration_test.go +++ b/server/internal/handlers/auth_handlers_integration_test.go @@ -4,8 +4,12 @@ package handlers_test import ( "encoding/json" + "io" "net/http" + "net/http/httptest" + "strings" "testing" + "time" "novamd/internal/handlers" "novamd/internal/models" @@ -25,40 +29,58 @@ func TestAuthHandlers_Integration(t *testing.T) { Password: "admin123", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, nil, nil) require.Equal(t, http.StatusOK, rr.Code) + // Verify all required cookies are present with correct attributes + cookies := rr.Result().Cookies() + var foundAccessToken, foundRefreshToken, foundCSRF bool + for _, cookie := range cookies { + switch cookie.Name { + case "access_token": + foundAccessToken = true + assert.True(t, cookie.HttpOnly, "access_token cookie must be HttpOnly") + assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite) + assert.Equal(t, 900, cookie.MaxAge) // 15 minutes + case "refresh_token": + foundRefreshToken = true + assert.True(t, cookie.HttpOnly, "refresh_token cookie must be HttpOnly") + assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite) + assert.Equal(t, 604800, cookie.MaxAge) // 7 days + case "csrf_token": + foundCSRF = true + assert.False(t, cookie.HttpOnly, "csrf_token cookie must not be HttpOnly") + assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite) + assert.Equal(t, 900, cookie.MaxAge) // 15 minutes + } + } + assert.True(t, foundAccessToken, "access_token cookie not found") + assert.True(t, foundRefreshToken, "refresh_token cookie not found") + assert.True(t, foundCSRF, "csrf_token cookie not found") + + // Verify CSRF token is in both cookie and header, and they match + var csrfCookie *http.Cookie + for _, cookie := range rr.Result().Cookies() { + if cookie.Name == "csrf_token" { + csrfCookie = cookie + break + } + } + require.NotNil(t, csrfCookie, "csrf_token cookie not found") + csrfHeader := rr.Header().Get("X-CSRF-Token") + assert.Equal(t, csrfCookie.Value, csrfHeader) + + // Verify response body var resp handlers.LoginResponse err := json.NewDecoder(rr.Body).Decode(&resp) require.NoError(t, err) - - assert.NotEmpty(t, resp.AccessToken) - assert.NotEmpty(t, resp.RefreshToken) + assert.NotEmpty(t, resp.SessionID) + assert.False(t, resp.ExpiresAt.IsZero()) assert.NotNil(t, resp.User) assert.Equal(t, loginReq.Email, resp.User.Email) assert.Equal(t, models.RoleAdmin, resp.User.Role) }) - t.Run("successful login - regular user", func(t *testing.T) { - loginReq := handlers.LoginRequest{ - Email: "user@test.com", - Password: "user123", - } - - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) - require.Equal(t, http.StatusOK, rr.Code) - - var resp handlers.LoginResponse - err := json.NewDecoder(rr.Body).Decode(&resp) - require.NoError(t, err) - - assert.NotEmpty(t, resp.AccessToken) - assert.NotEmpty(t, resp.RefreshToken) - assert.NotNil(t, resp.User) - assert.Equal(t, loginReq.Email, resp.User.Email) - assert.Equal(t, models.RoleEditor, resp.User.Role) - }) - t.Run("login failures", func(t *testing.T) { tests := []struct { name string @@ -97,12 +119,26 @@ func TestAuthHandlers_Integration(t *testing.T) { }, wantCode: http.StatusBadRequest, }, + { + name: "malformed JSON", + request: handlers.LoginRequest{}, // Will be overridden with bad JSON + wantCode: http.StatusBadRequest, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", tt.request, "", nil) + var rr *httptest.ResponseRecorder + if tt.name == "malformed JSON" { + // Need lower level helper to send malformed JSON + req := h.newRequest(t, http.MethodPost, "/api/v1/auth/login", nil) + req.Body = io.NopCloser(strings.NewReader("{bad json")) + rr = h.executeRequest(req) + } else { + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", tt.request, nil, nil) + } assert.Equal(t, tt.wantCode, rr.Code) + assert.Empty(t, rr.Result().Cookies(), "failed login should not set cookies") }) } }) @@ -110,58 +146,76 @@ func TestAuthHandlers_Integration(t *testing.T) { t.Run("refresh token", func(t *testing.T) { t.Run("successful token refresh", func(t *testing.T) { - // First login to get refresh token - loginReq := handlers.LoginRequest{ - Email: "user@test.com", - Password: "user123", - } - - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + // Need lower level helpers for precise cookie control + req := h.newRequest(t, http.MethodPost, "/api/v1/auth/refresh", nil) + h.addAuthCookies(t, req, h.RegularSession, true) // Adds both tokens + rr := h.executeRequest(req) require.Equal(t, http.StatusOK, rr.Code) - var loginResp handlers.LoginResponse - err := json.NewDecoder(rr.Body).Decode(&loginResp) - require.NoError(t, err) - - // Now try to refresh the token - refreshReq := handlers.RefreshRequest{ - RefreshToken: loginResp.RefreshToken, + // Verify new cookies + cookies := rr.Result().Cookies() + var foundAccessToken, foundCSRF bool + for _, cookie := range cookies { + switch cookie.Name { + case "access_token": + foundAccessToken = true + assert.Equal(t, 900, cookie.MaxAge) + case "csrf_token": + foundCSRF = true + assert.Equal(t, 900, cookie.MaxAge) + case "refresh_token": + t.Error("refresh token should not be renewed") + } } - - rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/refresh", refreshReq, "", nil) - require.Equal(t, http.StatusOK, rr.Code) - - var refreshResp handlers.RefreshResponse - err = json.NewDecoder(rr.Body).Decode(&refreshResp) - require.NoError(t, err) - assert.NotEmpty(t, refreshResp.AccessToken) + assert.True(t, foundAccessToken, "new access_token cookie not found") + assert.True(t, foundCSRF, "new csrf_token cookie not found") }) - t.Run("refresh failures", func(t *testing.T) { + t.Run("refresh token edge cases", func(t *testing.T) { tests := []struct { name string - request handlers.RefreshRequest + setup func(*http.Request) wantCode int }{ { - name: "invalid refresh token", - request: handlers.RefreshRequest{ - RefreshToken: "invalid-token", + name: "missing refresh token cookie", + setup: func(req *http.Request) { + // Only add access token + token, _ := h.JWTManager.GenerateAccessToken(h.RegularSession.UserID, "admin") + req.AddCookie(h.CookieManager.GenerateAccessTokenCookie(token)) + }, + wantCode: http.StatusBadRequest, + }, + { + name: "expired refresh token", + setup: func(req *http.Request) { + expiredSession := &models.Session{ + ID: "expired", + UserID: h.RegularUser.ID, + RefreshToken: "expired-token", + ExpiresAt: time.Now().Add(-1 * time.Hour), + } + h.addAuthCookies(t, req, expiredSession, true) }, wantCode: http.StatusUnauthorized, }, { - name: "empty refresh token", - request: handlers.RefreshRequest{ - RefreshToken: "", + name: "invalid refresh token format", + setup: func(req *http.Request) { + req.AddCookie(&http.Cookie{ + Name: "refresh_token", + Value: "invalid-format", + }) }, - wantCode: http.StatusBadRequest, + wantCode: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/refresh", tt.request, "", nil) + req := h.newRequest(t, http.MethodPost, "/api/v1/auth/refresh", nil) + tt.setup(req) + rr := h.executeRequest(req) assert.Equal(t, tt.wantCode, rr.Code) }) } @@ -170,63 +224,136 @@ func TestAuthHandlers_Integration(t *testing.T) { t.Run("logout", func(t *testing.T) { t.Run("successful logout", func(t *testing.T) { - // First login to get session - loginReq := handlers.LoginRequest{ - Email: "user@test.com", - Password: "user123", - } - - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) - require.Equal(t, http.StatusOK, rr.Code) - - var loginResp handlers.LoginResponse - err := json.NewDecoder(rr.Body).Decode(&loginResp) - require.NoError(t, err) - - // Now logout using session ID from login response - headers := map[string]string{ - "X-Session-ID": loginResp.Session.ID, - } - rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/logout", nil, loginResp.AccessToken, headers) + // Need CSRF token for POST request + req := h.newRequest(t, http.MethodPost, "/api/v1/auth/logout", nil) + csrfToken := h.addAuthCookies(t, req, h.RegularSession, true) + req.Header.Set("X-CSRF-Token", csrfToken) + rr := h.executeRequest(req) require.Equal(t, http.StatusNoContent, rr.Code) - // Try to use the refresh token - should fail - refreshReq := handlers.RefreshRequest{ - RefreshToken: loginResp.RefreshToken, + // Verify cookies are properly invalidated + for _, cookie := range rr.Result().Cookies() { + assert.True(t, cookie.MaxAge < 0, "cookie should be invalidated") + assert.True(t, cookie.Expires.Before(time.Now()), "cookie should be expired") } - rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/refresh", refreshReq, "", nil) + // Verify session is actually invalidated + rr = h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, h.RegularSession, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) - t.Run("logout without session ID", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodPost, "/api/v1/auth/logout", nil, h.RegularToken, nil) - assert.Equal(t, http.StatusBadRequest, rr.Code) + t.Run("logout edge cases", func(t *testing.T) { + tests := []struct { + name string + setup func(*http.Request) + wantCode int + }{ + { + name: "missing CSRF token", + setup: func(req *http.Request) { + h.addAuthCookies(t, req, h.RegularSession, true) + // Deliberately not setting X-CSRF-Token header + }, + wantCode: http.StatusForbidden, + }, + { + name: "mismatched CSRF token", + setup: func(req *http.Request) { + h.addAuthCookies(t, req, h.RegularSession, true) + req.Header.Set("X-CSRF-Token", "wrong-token") + }, + wantCode: http.StatusForbidden, + }, + { + name: "missing auth cookies", + setup: func(req *http.Request) { + // No setup - testing completely unauthenticated request + }, + wantCode: http.StatusUnauthorized, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := h.newRequest(t, http.MethodPost, "/api/v1/auth/logout", nil) + tt.setup(req) + rr := h.executeRequest(req) + assert.Equal(t, tt.wantCode, rr.Code) + }) + } }) }) t.Run("get current user", func(t *testing.T) { t.Run("successful get current user", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var user models.User err := json.NewDecoder(rr.Body).Decode(&user) require.NoError(t, err) - - assert.Equal(t, h.RegularUser.ID, user.ID) assert.Equal(t, h.RegularUser.Email, user.Email) - assert.Equal(t, h.RegularUser.Role, user.Role) }) - t.Run("get current user without token", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, "", nil) - assert.Equal(t, http.StatusUnauthorized, rr.Code) - }) + t.Run("auth edge cases", func(t *testing.T) { + tests := []struct { + name string + setup func(*http.Request) + wantCode int + }{ + { + name: "missing auth cookie", + setup: func(req *http.Request) { + // No setup - testing unauthenticated request + }, + wantCode: http.StatusUnauthorized, + }, + { + name: "invalid session ID", + setup: func(req *http.Request) { + invalidSession := &models.Session{ + ID: "invalid", + UserID: 999, + RefreshToken: "invalid", + ExpiresAt: time.Now().Add(time.Hour), + } + h.addAuthCookies(t, req, invalidSession, false) + }, + wantCode: http.StatusUnauthorized, + }, + { + name: "expired session", + setup: func(req *http.Request) { + expiredSession := &models.Session{ + ID: "expired", + UserID: h.RegularUser.ID, + RefreshToken: "expired-token", + ExpiresAt: time.Now().Add(-1 * time.Hour), + } + h.addAuthCookies(t, req, expiredSession, false) + }, + wantCode: http.StatusUnauthorized, + }, + { + name: "malformed access token", + setup: func(req *http.Request) { + req.AddCookie(&http.Cookie{ + Name: "access_token", + Value: "malformed-token", + }) + }, + wantCode: http.StatusUnauthorized, + }, + } - t.Run("get current user with invalid token", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, "invalid-token", nil) - assert.Equal(t, http.StatusUnauthorized, rr.Code) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := h.newRequest(t, http.MethodGet, "/api/v1/auth/me", nil) + tt.setup(req) + rr := h.executeRequest(req) + assert.Equal(t, tt.wantCode, rr.Code) + }) + } }) }) } diff --git a/server/internal/handlers/file_handlers_integration_test.go b/server/internal/handlers/file_handlers_integration_test.go index 516c508..e317575 100644 --- a/server/internal/handlers/file_handlers_integration_test.go +++ b/server/internal/handlers/file_handlers_integration_test.go @@ -27,7 +27,7 @@ func TestFileHandlers_Integration(t *testing.T) { UserID: h.RegularUser.ID, Name: "File Test Workspace", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) err := json.NewDecoder(rr.Body).Decode(workspace) @@ -37,7 +37,7 @@ func TestFileHandlers_Integration(t *testing.T) { baseURL := fmt.Sprintf("/api/v1/workspaces/%s/files", url.PathEscape(workspace.Name)) t.Run("list empty directory", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var files []storage.FileNode @@ -51,16 +51,16 @@ func TestFileHandlers_Integration(t *testing.T) { filePath := "test.md" // Save file - rr := h.makeRequestRaw(t, http.MethodPost, baseURL+"/"+filePath, strings.NewReader(content), h.RegularToken, nil) + rr := h.makeRequestRaw(t, http.MethodPost, baseURL+"/"+filePath, strings.NewReader(content), h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) // Get file content - rr = h.makeRequest(t, http.MethodGet, baseURL+"/"+filePath, nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, baseURL+"/"+filePath, nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, content, rr.Body.String()) // List directory should now show the file - rr = h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var files []storage.FileNode @@ -80,12 +80,12 @@ func TestFileHandlers_Integration(t *testing.T) { // Create all files for path, content := range files { - rr := h.makeRequest(t, http.MethodPost, baseURL+"/"+path, content, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/"+path, content, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) } // List all files - rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var fileNodes []storage.FileNode @@ -116,11 +116,11 @@ func TestFileHandlers_Integration(t *testing.T) { // Look up a file that exists in multiple locations filename := "readme.md" dupContent := "Another readme" - rr := h.makeRequest(t, http.MethodPost, baseURL+"/projects/"+filename, dupContent, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/projects/"+filename, dupContent, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) // Search for the file - rr = h.makeRequest(t, http.MethodGet, baseURL+"/lookup?filename="+filename, nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, baseURL+"/lookup?filename="+filename, nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response struct { @@ -131,7 +131,7 @@ func TestFileHandlers_Integration(t *testing.T) { assert.Len(t, response.Paths, 2) // Search for non-existent file - rr = h.makeRequest(t, http.MethodGet, baseURL+"/lookup?filename=nonexistent.md", nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, baseURL+"/lookup?filename=nonexistent.md", nil, h.RegularSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) @@ -140,21 +140,21 @@ func TestFileHandlers_Integration(t *testing.T) { content := "This file will be deleted" // Create file - rr := h.makeRequest(t, http.MethodPost, baseURL+"/"+filePath, content, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/"+filePath, content, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) // Delete file - rr = h.makeRequest(t, http.MethodDelete, baseURL+"/"+filePath, nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodDelete, baseURL+"/"+filePath, nil, h.RegularSession, nil) require.Equal(t, http.StatusNoContent, rr.Code) // Verify file is gone - rr = h.makeRequest(t, http.MethodGet, baseURL+"/"+filePath, nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, baseURL+"/"+filePath, nil, h.RegularSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) t.Run("last opened file", func(t *testing.T) { // Initially should be empty - rr := h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response struct { @@ -170,11 +170,11 @@ func TestFileHandlers_Integration(t *testing.T) { }{ FilePath: "docs/readme.md", } - rr = h.makeRequest(t, http.MethodPut, baseURL+"/last", updateReq, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodPut, baseURL+"/last", updateReq, h.RegularSession, nil) require.Equal(t, http.StatusNoContent, rr.Code) // Verify update - rr = h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) err = json.NewDecoder(rr.Body).Decode(&response) @@ -183,7 +183,7 @@ func TestFileHandlers_Integration(t *testing.T) { // Test invalid file path updateReq.FilePath = "nonexistent.md" - rr = h.makeRequest(t, http.MethodPut, baseURL+"/last", updateReq, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodPut, baseURL+"/last", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) @@ -204,12 +204,12 @@ func TestFileHandlers_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // Test without token - rr := h.makeRequest(t, tc.method, tc.path, tc.body, "", nil) + // Test without session + rr := h.makeRequest(t, tc.method, tc.path, tc.body, nil, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) - // Test with wrong user's token - rr = h.makeRequest(t, tc.method, tc.path, tc.body, h.AdminToken, nil) + // Test with wrong user's session + rr = h.makeRequest(t, tc.method, tc.path, tc.body, h.AdminSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) } @@ -226,11 +226,11 @@ func TestFileHandlers_Integration(t *testing.T) { for _, path := range maliciousPaths { t.Run(path, func(t *testing.T) { // Try to read - rr := h.makeRequest(t, http.MethodGet, baseURL+"/"+path, nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, baseURL+"/"+path, nil, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) // Try to write - rr = h.makeRequest(t, http.MethodPost, baseURL+"/"+path, "malicious content", h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodPost, baseURL+"/"+path, "malicious content", h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) } diff --git a/server/internal/handlers/git_handlers_integration_test.go b/server/internal/handlers/git_handlers_integration_test.go index c7706c8..f754974 100644 --- a/server/internal/handlers/git_handlers_integration_test.go +++ b/server/internal/handlers/git_handlers_integration_test.go @@ -32,7 +32,7 @@ func TestGitHandlers_Integration(t *testing.T) { GitCommitMsgTemplate: "Update: {{message}}", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) err := json.NewDecoder(rr.Body).Decode(workspace) @@ -50,7 +50,7 @@ func TestGitHandlers_Integration(t *testing.T) { "message": commitMsg, } - rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response map[string]string @@ -70,7 +70,7 @@ func TestGitHandlers_Integration(t *testing.T) { "message": "", } - rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, 0, h.MockGit.GetCommitCount(), "Commit should not be called") }) @@ -83,7 +83,7 @@ func TestGitHandlers_Integration(t *testing.T) { "message": "Test message", } - rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularSession, nil) assert.Equal(t, http.StatusInternalServerError, rr.Code) h.MockGit.SetError(nil) // Reset error state @@ -94,7 +94,7 @@ func TestGitHandlers_Integration(t *testing.T) { h.MockGit.Reset() t.Run("successful pull", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodPost, baseURL+"/pull", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/pull", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response map[string]string @@ -109,7 +109,7 @@ func TestGitHandlers_Integration(t *testing.T) { h.MockGit.Reset() h.MockGit.SetError(fmt.Errorf("mock git error")) - rr := h.makeRequest(t, http.MethodPost, baseURL+"/pull", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, baseURL+"/pull", nil, h.RegularSession, nil) assert.Equal(t, http.StatusInternalServerError, rr.Code) h.MockGit.SetError(nil) // Reset error state @@ -140,12 +140,12 @@ func TestGitHandlers_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // Test without token - rr := h.makeRequest(t, tc.method, tc.path, tc.body, "", nil) + // Test without session + rr := h.makeRequest(t, tc.method, tc.path, tc.body, nil, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) - // Test with wrong user's token - rr = h.makeRequest(t, tc.method, tc.path, tc.body, h.AdminToken, nil) + // Test with wrong user's session + rr = h.makeRequest(t, tc.method, tc.path, tc.body, h.AdminSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) } @@ -160,7 +160,7 @@ func TestGitHandlers_Integration(t *testing.T) { Name: "Non-Git Workspace", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", nonGitWorkspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", nonGitWorkspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) err := json.NewDecoder(rr.Body).Decode(nonGitWorkspace) @@ -170,11 +170,11 @@ func TestGitHandlers_Integration(t *testing.T) { // Try to commit commitMsg := map[string]string{"message": "test"} - rr = h.makeRequest(t, http.MethodPost, nonGitBaseURL+"/commit", commitMsg, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodPost, nonGitBaseURL+"/commit", commitMsg, h.RegularSession, nil) assert.Equal(t, http.StatusInternalServerError, rr.Code) // Try to pull - rr = h.makeRequest(t, http.MethodPost, nonGitBaseURL+"/pull", nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodPost, nonGitBaseURL+"/pull", nil, h.RegularSession, nil) assert.Equal(t, http.StatusInternalServerError, rr.Code) }) }) diff --git a/server/internal/handlers/integration_test.go b/server/internal/handlers/integration_test.go index 4d8b6aa..5377a8f 100644 --- a/server/internal/handlers/integration_test.go +++ b/server/internal/handlers/integration_test.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/json" "io" + "net/http" "net/http/httptest" "os" "testing" @@ -29,10 +30,11 @@ type testHarness struct { Storage storage.Manager JWTManager auth.JWTManager SessionManager auth.SessionManager + CookieManager auth.CookieManager AdminUser *models.User - AdminToken string + AdminSession *models.Session RegularUser *models.User - RegularToken string + RegularSession *models.Session TempDirectory string MockGit *MockGitClient } @@ -86,6 +88,9 @@ func setupTestHarness(t *testing.T) *testHarness { // Initialize session service sessionSvc := auth.NewSessionService(database, jwtSvc) + // Initialize cookie service + cookieSvc := auth.NewCookieService(true, "localhost") + // Create test config testConfig := &app.Config{ DBPath: ":memory:", @@ -116,18 +121,19 @@ func setupTestHarness(t *testing.T) *testHarness { Storage: storageSvc, JWTManager: jwtSvc, SessionManager: sessionSvc, + CookieManager: cookieSvc, TempDirectory: tempDir, MockGit: mockGit, } // Create test users - adminUser, adminToken := h.createTestUser(t, "admin@test.com", "admin123", models.RoleAdmin) - regularUser, regularToken := h.createTestUser(t, "user@test.com", "user123", models.RoleEditor) + adminUser, adminSession := h.createTestUser(t, "admin@test.com", "admin123", models.RoleAdmin) + regularUser, regularSession := h.createTestUser(t, "user@test.com", "user123", models.RoleEditor) h.AdminUser = adminUser - h.AdminToken = adminToken + h.AdminSession = adminSession h.RegularUser = regularUser - h.RegularToken = regularToken + h.RegularSession = regularSession return h } @@ -146,7 +152,7 @@ func (h *testHarness) teardown(t *testing.T) { } // createTestUser creates a test user and returns the user and access token -func (h *testHarness) createTestUser(t *testing.T, email, password string, role models.UserRole) (*models.User, string) { +func (h *testHarness) createTestUser(t *testing.T, email, password string, role models.UserRole) (*models.User, *models.Session) { t.Helper() hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -172,25 +178,19 @@ func (h *testHarness) createTestUser(t *testing.T, email, password string, role t.Fatalf("Failed to initialize user workspace: %v", err) } - session, accessToken, err := h.SessionManager.CreateSession(user.ID, string(user.Role)) + session, _, err := h.SessionManager.CreateSession(user.ID, string(user.Role)) if err != nil { t.Fatalf("Failed to create session: %v", err) } - if session == nil || accessToken == "" { - t.Fatal("Failed to get valid session or token") - } - - return user, accessToken + return user, session } -// makeRequest is a helper function to make HTTP requests in tests -func (h *testHarness) makeRequest(t *testing.T, method, path string, body interface{}, token string, headers map[string]string) *httptest.ResponseRecorder { +func (h *testHarness) newRequest(t *testing.T, method, path string, body interface{}) *http.Request { t.Helper() var reqBody []byte var err error - if body != nil { reqBody, err = json.Marshal(body) if err != nil { @@ -199,38 +199,87 @@ func (h *testHarness) makeRequest(t *testing.T, method, path string, body interf } req := httptest.NewRequest(method, path, bytes.NewBuffer(reqBody)) - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } req.Header.Set("Content-Type", "application/json") + return req +} - // Add any additional headers - for k, v := range headers { - req.Header.Set(k, v) - } +// newRequestRaw creates a new request with raw body +func (h *testHarness) newRequestRaw(t *testing.T, method, path string, body io.Reader) *http.Request { + t.Helper() + return httptest.NewRequest(method, path, body) +} +// executeRequest executes the request and returns response recorder +func (h *testHarness) executeRequest(req *http.Request) *httptest.ResponseRecorder { rr := httptest.NewRecorder() h.Server.Router().ServeHTTP(rr, req) - return rr } -// makeRequestRaw is a helper function to make HTTP requests with raw body content -func (h *testHarness) makeRequestRaw(t *testing.T, method, path string, body io.Reader, token string, headers map[string]string) *httptest.ResponseRecorder { +// addAuthCookies adds authentication cookies to request +func (h *testHarness) addAuthCookies(t *testing.T, req *http.Request, session *models.Session, addCSRF bool) string { t.Helper() - req := httptest.NewRequest(method, path, body) - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) + if session == nil { + return "" } - // Add any additional headers + accessToken, err := h.JWTManager.GenerateAccessToken(session.UserID, "admin") + if err != nil { + t.Fatalf("Failed to generate access token: %v", err) + } + + req.AddCookie(h.CookieManager.GenerateAccessTokenCookie(accessToken)) + req.AddCookie(h.CookieManager.GenerateRefreshTokenCookie(session.RefreshToken)) + + if addCSRF { + csrfToken := "test-csrf-token" + req.AddCookie(h.CookieManager.GenerateCSRFCookie(csrfToken)) + return csrfToken + } + return "" +} + +// makeRequest is the main helper for making JSON requests +func (h *testHarness) makeRequest(t *testing.T, method, path string, body interface{}, session *models.Session, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + + req := h.newRequest(t, method, path, body) + + // Add custom headers for k, v := range headers { req.Header.Set(k, v) } - rr := httptest.NewRecorder() - h.Server.Router().ServeHTTP(rr, req) + if session != nil { + needsCSRF := method != http.MethodGet && method != http.MethodHead && method != http.MethodOptions + csrfToken := h.addAuthCookies(t, req, session, needsCSRF) + if needsCSRF { + req.Header.Set("X-CSRF-Token", csrfToken) + } + } - return rr + return h.executeRequest(req) +} + +// makeRequestRawWithHeaders adds support for custom headers with raw body +func (h *testHarness) makeRequestRaw(t *testing.T, method, path string, body io.Reader, session *models.Session, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + + req := h.newRequestRaw(t, method, path, body) + + // Add custom headers + for k, v := range headers { + req.Header.Set(k, v) + } + + if session != nil { + needsCSRF := method != http.MethodGet && method != http.MethodHead && method != http.MethodOptions + csrfToken := h.addAuthCookies(t, req, session, needsCSRF) + if needsCSRF { + req.Header.Set("X-CSRF-Token", csrfToken) + } + } + + return h.executeRequest(req) } diff --git a/server/internal/handlers/user_handlers_integration_test.go b/server/internal/handlers/user_handlers_integration_test.go index 9c41772..378703e 100644 --- a/server/internal/handlers/user_handlers_integration_test.go +++ b/server/internal/handlers/user_handlers_integration_test.go @@ -23,7 +23,7 @@ func TestUserHandlers_Integration(t *testing.T) { t.Run("get current user", func(t *testing.T) { t.Run("successful get", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var user models.User @@ -38,7 +38,7 @@ func TestUserHandlers_Integration(t *testing.T) { }) t.Run("unauthorized", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, "", nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, nil, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) }) @@ -49,7 +49,7 @@ func TestUserHandlers_Integration(t *testing.T) { DisplayName: "Updated Name", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var user models.User @@ -64,7 +64,7 @@ func TestUserHandlers_Integration(t *testing.T) { CurrentPassword: currentPassword, } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var user models.User @@ -80,7 +80,7 @@ func TestUserHandlers_Integration(t *testing.T) { Email: "anotheremail@test.com", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) @@ -90,7 +90,7 @@ func TestUserHandlers_Integration(t *testing.T) { CurrentPassword: "wrongpassword", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) @@ -100,7 +100,7 @@ func TestUserHandlers_Integration(t *testing.T) { NewPassword: "newpassword123", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) // Verify can login with new password @@ -109,7 +109,7 @@ func TestUserHandlers_Integration(t *testing.T) { Password: "newpassword123", } - rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, nil, nil) assert.Equal(t, http.StatusOK, rr.Code) currentPassword = updateReq.NewPassword @@ -120,7 +120,7 @@ func TestUserHandlers_Integration(t *testing.T) { NewPassword: "newpass123", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) @@ -130,7 +130,7 @@ func TestUserHandlers_Integration(t *testing.T) { NewPassword: "newpass123", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) @@ -140,7 +140,7 @@ func TestUserHandlers_Integration(t *testing.T) { NewPassword: "short", } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) @@ -150,7 +150,7 @@ func TestUserHandlers_Integration(t *testing.T) { CurrentPassword: currentPassword, } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularSession, nil) assert.Equal(t, http.StatusConflict, rr.Code) }) }) @@ -164,37 +164,44 @@ func TestUserHandlers_Integration(t *testing.T) { Role: models.RoleEditor, } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminSession, nil) require.Equal(t, http.StatusOK, rr.Code) var newUser models.User err := json.NewDecoder(rr.Body).Decode(&newUser) require.NoError(t, err) - // Get token for new user + // Get session for new user loginReq := handlers.LoginRequest{ Email: createReq.Email, Password: createReq.Password, } - rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, nil, nil) require.Equal(t, http.StatusOK, rr.Code) var loginResp handlers.LoginResponse err = json.NewDecoder(rr.Body).Decode(&loginResp) require.NoError(t, err) - userToken := loginResp.AccessToken + + // Create a session struct for the new user + userSession := &models.Session{ + ID: loginResp.SessionID, + UserID: newUser.ID, + RefreshToken: "", + ExpiresAt: loginResp.ExpiresAt, + } t.Run("successful delete", func(t *testing.T) { deleteReq := handlers.DeleteAccountRequest{ Password: createReq.Password, } - rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, userToken, nil) + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, userSession, nil) require.Equal(t, http.StatusNoContent, rr.Code) // Verify user is deleted - rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, nil, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) @@ -203,7 +210,7 @@ func TestUserHandlers_Integration(t *testing.T) { Password: "wrongpassword", } - rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, h.RegularSession, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) @@ -212,7 +219,7 @@ func TestUserHandlers_Integration(t *testing.T) { Password: "admin123", // Admin password from test harness } - rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, h.AdminToken, nil) + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, h.AdminSession, nil) assert.Equal(t, http.StatusForbidden, rr.Code) }) }) diff --git a/server/internal/handlers/workspace_handlers_integration_test.go b/server/internal/handlers/workspace_handlers_integration_test.go index e07efc0..e973b83 100644 --- a/server/internal/handlers/workspace_handlers_integration_test.go +++ b/server/internal/handlers/workspace_handlers_integration_test.go @@ -20,7 +20,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { t.Run("list workspaces", func(t *testing.T) { t.Run("successful list", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var workspaces []*models.Workspace @@ -30,7 +30,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { }) t.Run("unauthorized", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, "", nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, nil, nil) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) }) @@ -41,7 +41,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { Name: "Test Workspace", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var created models.Workspace @@ -64,7 +64,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { GitCommitEmail: "test@example.com", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var created models.Workspace @@ -86,7 +86,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { // Missing required Git settings } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) }) @@ -95,7 +95,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { workspace := &models.Workspace{ Name: "Test Workspace Operations", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) err := json.NewDecoder(rr.Body).Decode(workspace) require.NoError(t, err) @@ -105,7 +105,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { t.Run("get workspace", func(t *testing.T) { t.Run("successful get", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var got models.Workspace @@ -116,13 +116,13 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { }) t.Run("nonexistent workspace", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/nonexistent", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/nonexistent", nil, h.RegularSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) t.Run("unauthorized access", func(t *testing.T) { // Try accessing with another user's token - rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.AdminToken, nil) + rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.AdminSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) }) @@ -131,7 +131,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { t.Run("update name", func(t *testing.T) { workspace.Name = "Updated Workspace" - rr := h.makeRequest(t, http.MethodPut, baseURL, workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, baseURL, workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var updated models.Workspace @@ -152,7 +152,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { ShowHiddenFiles: true, } - rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var updated models.Workspace @@ -176,7 +176,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { GitCommitEmail: "test@example.com", } - rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var updated models.Workspace @@ -200,14 +200,14 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { // Missing required Git settings } - rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) }) t.Run("last workspace", func(t *testing.T) { t.Run("get last workspace", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response struct { @@ -225,11 +225,11 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { WorkspaceName: workspace.Name, } - rr := h.makeRequest(t, http.MethodPut, "/api/v1/workspaces/last", req, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPut, "/api/v1/workspaces/last", req, h.RegularSession, nil) require.Equal(t, http.StatusNoContent, rr.Code) // Verify the update - rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response struct { @@ -243,7 +243,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { t.Run("delete workspace", func(t *testing.T) { // Get current workspaces to know how many we have - rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var existingWorkspaces []*models.Workspace @@ -254,13 +254,13 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { newWorkspace := &models.Workspace{ Name: "Workspace To Delete", } - rr = h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", newWorkspace, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", newWorkspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) err = json.NewDecoder(rr.Body).Decode(newWorkspace) require.NoError(t, err) t.Run("successful delete", func(t *testing.T) { - rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(newWorkspace.Name), nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(newWorkspace.Name), nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) var response struct { @@ -271,7 +271,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { assert.NotEmpty(t, response.NextWorkspaceName) // Verify workspace is deleted - rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/"+url.PathEscape(newWorkspace.Name), nil, h.RegularToken, nil) + rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/"+url.PathEscape(newWorkspace.Name), nil, h.RegularSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) @@ -279,13 +279,13 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { // Delete all but one workspace for i := 0; i < len(existingWorkspaces)-1; i++ { ws := existingWorkspaces[i] - rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(ws.Name), nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(ws.Name), nil, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) } // Try to delete the last remaining workspace lastWs := existingWorkspaces[len(existingWorkspaces)-1] - rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(lastWs.Name), nil, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(lastWs.Name), nil, h.RegularSession, nil) assert.Equal(t, http.StatusBadRequest, rr.Code) }) @@ -294,11 +294,11 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { workspace := &models.Workspace{ Name: "Unauthorized Delete Test", } - rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularSession, nil) require.Equal(t, http.StatusOK, rr.Code) // Try to delete with wrong user's token - rr = h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(workspace.Name), nil, h.AdminToken, nil) + rr = h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(workspace.Name), nil, h.AdminSession, nil) assert.Equal(t, http.StatusNotFound, rr.Code) }) })