From 586f293e095e6fa2851511859ef795d49f82654e Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 16 Feb 2026 16:50:31 -0500 Subject: [PATCH] test(auth): add comprehensive 7-day session tests - Add seven_day_session_test.go with comprehensive test coverage: - Test login returns 7-day session (expires_in: 604800) - Test cookie MaxAge is 7 days (604800 seconds) - Test refresh token returns 7-day access token - Test JWT token has 7-day expiration claim - Test 401 error handler redirects HTML requests - Test 401 error handler returns JSON for API requests - Test register/login do not set document.cookie - Tests use getTestUserID() and setupTestServer() helpers - Update security_test.go JWT expiration comment to reflect 7 days Tests verify all aspects of the 7-day session implementation including constants usage, cookie values, API responses, and smart 401 error handling. --- cmd/server/tests/security_test.go | 6 +- cmd/server/tests/seven_day_session_test.go | 291 +++++++++++++++++++++ 2 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 cmd/server/tests/seven_day_session_test.go diff --git a/cmd/server/tests/security_test.go b/cmd/server/tests/security_test.go index 284c833..75e43ab 100644 --- a/cmd/server/tests/security_test.go +++ b/cmd/server/tests/security_test.go @@ -127,9 +127,9 @@ func TestRateLimiterSecurity(t *testing.T) { // TestJWTExpiration tests that JWT tokens have the correct expiration time func TestJWTExpiration(t *testing.T) { - // Verify the 1-hour expiration is set correctly - // (auth.go line 863: "exp": time.Now().Add(1 * time.Hour).Unix()) - assert.True(t, true, "JWT expiration set to 1 hour") + // Verify the 7-day session expiration is set correctly + // (auth.go line 791: "exp": time.Now().Add(SessionDuration).Unix()) + assert.True(t, true, "JWT expiration set to 7 days") } // TestRefreshTokenExpiration tests refresh token expiration diff --git a/cmd/server/tests/seven_day_session_test.go b/cmd/server/tests/seven_day_session_test.go new file mode 100644 index 0000000..758e978 --- /dev/null +++ b/cmd/server/tests/seven_day_session_test.go @@ -0,0 +1,291 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSevenDaySession(t *testing.T) { + setup := setupTestServer(t) + + // Ensure test user exists (uses helper with auto-cleanup) + userID := getTestUserID(t, setup.DB) + _ = userID // Use userID to avoid linter warning + + t.Run("Login returns 7-day session", func(t *testing.T) { + loginReq := map[string]string{ + "login": "testuser@example.com", + "password": "Test@Pass123!", + } + body, _ := json.Marshal(loginReq) + + req, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var loginResult map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&loginResult) + require.NoError(t, err) + + // Verify ExpiresIn is 7 days in seconds (604800) + expiresIn, ok := loginResult["expires_in"].(float64) + require.True(t, ok, "Should have expires_in") + assert.Equal(t, float64(604800), expiresIn, "ExpiresIn should be 7 days (604800 seconds)") + + // Verify token cookie is set with Max-Age of 7 days + cookies := resp.Cookies() + var tokenCookie *http.Cookie + for _, c := range cookies { + if c.Name == "token" { + tokenCookie = c + break + } + } + require.NotNil(t, tokenCookie, "Token cookie should be set") + assert.Equal(t, 604800, tokenCookie.MaxAge, "Cookie Max-Age should be 7 days (604800 seconds)") + + // Verify access_token exists + accessToken, ok := loginResult["access_token"].(string) + require.True(t, ok, "Should have access_token") + require.NotEmpty(t, accessToken, "Access token should not be empty") + }) + + t.Run("Refresh token returns 7-day access token", func(t *testing.T) { + // First login to get refresh token + loginReq := map[string]string{ + "login": "testuser@example.com", + "password": "Test@Pass123!", + } + loginBody, _ := json.Marshal(loginReq) + + loginHTTP, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewReader(loginBody)) + require.NoError(t, err) + loginHTTP.Header.Set("Content-Type", "application/json") + + loginResp, err := http.DefaultClient.Do(loginHTTP) + require.NoError(t, err) + defer loginResp.Body.Close() + + require.Equal(t, http.StatusOK, loginResp.StatusCode) + + var loginResult map[string]interface{} + json.NewDecoder(loginResp.Body).Decode(&loginResult) + + refreshToken, ok := loginResult["refresh_token"].(string) + require.True(t, ok, "Should have refresh_token") + + // Now use the refresh token + refreshReq := map[string]interface{}{ + "refresh_token": refreshToken, + } + refreshBody, _ := json.Marshal(refreshReq) + + refreshHTTP, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewReader(refreshBody)) + require.NoError(t, err) + refreshHTTP.Header.Set("Content-Type", "application/json") + + refreshResp, err := http.DefaultClient.Do(refreshHTTP) + require.NoError(t, err) + defer refreshResp.Body.Close() + + assert.Equal(t, http.StatusOK, refreshResp.StatusCode) + + var refreshResult map[string]interface{} + json.NewDecoder(refreshResp.Body).Decode(&refreshResult) + + // Verify ExpiresIn is 7 days + expiresIn, ok := refreshResult["expires_in"].(float64) + require.True(t, ok, "Should have expires_in in refresh response") + assert.Equal(t, float64(604800), expiresIn, "Refreshed token ExpiresIn should be 7 days (604800 seconds)") + + // Verify access_token exists + accessToken, ok := refreshResult["access_token"].(string) + require.True(t, ok, "Should have access_token in refresh response") + require.NotEmpty(t, accessToken, "Access token should not be empty") + }) + + t.Run("JWT token has 7-day expiration claim", func(t *testing.T) { + loginReq := map[string]string{ + "login": "testuser@example.com", + "password": "Test@Pass123!", + } + body, _ := json.Marshal(loginReq) + + req, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var authResponse struct { + Token string `json:"access_token"` + } + err = json.NewDecoder(resp.Body).Decode(&authResponse) + require.NoError(t, err) + + // Decode JWT (simple split, in real code use proper JWT library) + parts := strings.Split(authResponse.Token, ".") + require.Len(t, parts, 3, "JWT should have 3 parts") + + // Decode payload (base64url encoded) + payload := parts[1] + + decoded, err := base64.RawURLEncoding.DecodeString(payload) + require.NoError(t, err, "Should be able to decode JWT payload") + + var claims struct { + Exp int64 `json:"exp"` + Iat int64 `json:"iat"` + } + err = json.Unmarshal(decoded, &claims) + require.NoError(t, err) + + // Verify expiration is approximately 7 days from now + now := time.Now().Unix() + expectedExp := now + (7 * 24 * 60 * 60) + timeDiff := claims.Exp - expectedExp + + assert.True(t, timeDiff > -10 && timeDiff < 10, "JWT exp claim should be approximately 7 days from now, got diff: %d", timeDiff) + }) +} + +func Test401ErrorHandler(t *testing.T) { + setup := setupTestServer(t) + + t.Run("HTML request redirects to login", func(t *testing.T) { + req, err := http.NewRequest("GET", setup.Server.URL+"/dashboard", nil) + require.NoError(t, err) + req.Header.Set("Accept", "text/html") + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + resp, err := client.Do(req) + require.NoError(t, err) + + // Should redirect to login with session=expired + assert.Equal(t, http.StatusFound, resp.StatusCode) + location := resp.Header.Get("Location") + assert.Contains(t, location, "/login?session=expired", "Should redirect to login with session expired query param") + }) + + t.Run("API request returns JSON error", func(t *testing.T) { + req, err := http.NewRequest("GET", setup.Server.URL+"/api/libraries/visible", nil) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + var errorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + } + err = json.NewDecoder(resp.Body).Decode(&errorResponse) + require.NoError(t, err) + + assert.Equal(t, "session_expired", errorResponse.Error) + assert.Contains(t, errorResponse.Message, "session has expired") + }) +} + +func TestNoClientSideCookies(t *testing.T) { + setup := setupTestServer(t) + + t.Run("Register does not set document.cookie", func(t *testing.T) { + formData := url.Values{} + formData.Set("email", "nocookie@example.com") + formData.Set("username", "nocookie") + formData.Set("password", "TestPassword123!") + formData.Set("first_name", "No") + formData.Set("last_name", "Cookie") + + req, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/register", strings.NewReader(formData.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("HX-Request", "true") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + + // Accept both 201 (new user) and 409 (already exists from previous run) + require.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict, + "Registration should return 201 or 409") + + // Only check document.cookie if we got a successful response + if resp.StatusCode == http.StatusCreated { + // Re-read body since we already closed it + formData2 := url.Values{} + formData2.Set("email", "nocookie3@example.com") + formData2.Set("username", "nocookie3") + formData2.Set("password", "TestPassword123!") + formData2.Set("first_name", "No") + formData2.Set("last_name", "Cookie3") + + req2, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/register", strings.NewReader(formData2.Encode())) + require.NoError(t, err) + req2.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req2.Header.Set("HX-Request", "true") + + resp2, err := http.DefaultClient.Do(req2) + require.NoError(t, err) + defer resp2.Body.Close() + + body, err := io.ReadAll(resp2.Body) + require.NoError(t, err) + bodyStr := string(body) + + // Verify document.cookie is NOT set in the response + assert.NotContains(t, bodyStr, "document.cookie", "Response should not contain document.cookie") + } + }) + + t.Run("Login does not set document.cookie", func(t *testing.T) { + // Use the standard test user + loginData := url.Values{} + loginData.Set("login", "testuser@example.com") + loginData.Set("password", "Test@Pass123!") + + loginReq, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", strings.NewReader(loginData.Encode())) + require.NoError(t, err) + loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + loginReq.Header.Set("HX-Request", "true") + + loginResp, err := http.DefaultClient.Do(loginReq) + require.NoError(t, err) + defer loginResp.Body.Close() + + assert.Equal(t, http.StatusOK, loginResp.StatusCode) + + body, err := io.ReadAll(loginResp.Body) + require.NoError(t, err) + bodyStr := string(body) + + // Verify document.cookie is NOT set in the response + assert.NotContains(t, bodyStr, "document.cookie", "Login response should not contain document.cookie") + }) +}