Update email domain in remaining test files: - device_cap_test.go - device_test.go - queue_test.go - refresh_token_test.go - seven_day_session_test.go All test files now consistently use the dedicated test domain to prevent conflicts with real user data.
292 lines
9.5 KiB
Go
292 lines
9.5 KiB
Go
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@tests.bookhoard.internal",
|
|
"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@tests.bookhoard.internal",
|
|
"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@tests.bookhoard.internal",
|
|
"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@tests.bookhoard.internal")
|
|
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")
|
|
})
|
|
}
|