Files
bookhoard/cmd/server/tests/refresh_token_test.go
T
john-okeefe a6700f73e0 fix(tests): handle all Close() and Decode() errors across integration tests
Replace all unhandled resp.Body.Close() calls throughout the test suite:

- Deferred calls: replace 'defer VAR.Body.Close()' with a closure that explicitly
  discards the error via 'defer func(Body io.ReadCloser) { _ = Body.Close() }(VAR.Body)'
- Immediate calls: replace 'VAR.Body.Close()' with '_ = VAR.Body.Close()'

Replace all unhandled json.NewDecoder(VAR.Body).Decode(&x) calls with error capture
and require.NoError assertion. Files using httptest.ResponseRecorder (collections_preview,
processing_issues) use 'err :=' declaration; suite-style tests (scanner_integration,
dashboard_integration) use s.T() instead of t.
2026-04-21 20:33:05 -04:00

354 lines
11 KiB
Go

package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestRefreshTokenFlow comprehensive tests for token refresh functionality
func TestRefreshTokenFlow(t *testing.T) {
setup := setupTestServer(t)
client := &http.Client{}
t.Run("RefreshToken_MissingToken", func(t *testing.T) {
req := map[string]interface{}{}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("RefreshToken_InvalidTokenFormat", func(t *testing.T) {
req := map[string]interface{}{
"refresh_token": "not-a-valid-jwt-token",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("RefreshToken_ValidToken", func(t *testing.T) {
// First, login to get tokens
loginReq := map[string]string{
"login": "testuser@tests.bookhoard.internal",
"password": "Test@Pass123!",
}
loginBody, _ := json.Marshal(loginReq)
loginHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
loginHTTP.Header.Set("Content-Type", "application/json")
loginResp, err := client.Do(loginHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(loginResp.Body)
require.Equal(t, http.StatusOK, loginResp.StatusCode)
var loginResult map[string]interface{}
err = json.NewDecoder(loginResp.Body).Decode(&loginResult)
require.NoError(t, err)
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, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
refreshHTTP.Header.Set("Content-Type", "application/json")
refreshResp, err := client.Do(refreshHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(refreshResp.Body)
assert.Equal(t, http.StatusOK, refreshResp.StatusCode)
var refreshResult map[string]interface{}
err = json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
require.NoError(t, err)
assert.Contains(t, refreshResult, "access_token")
assert.NotEmpty(t, refreshResult["access_token"], "New access token should not be empty")
// The new access token should be different from the original
newAccessToken := refreshResult["access_token"].(string)
originalAccessToken := loginResult["access_token"].(string)
assert.NotEqual(t, originalAccessToken, newAccessToken, "New access token should be different")
})
t.Run("RefreshToken_InvalidRequestBody", func(t *testing.T) {
// Send invalid JSON
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer([]byte("invalid json")))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("RefreshToken_MissingContentType", func(t *testing.T) {
req := map[string]interface{}{
"refresh_token": "some-token",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(body))
// Don't set Content-Type
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
// Should still work or return appropriate error
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnsupportedMediaType)
})
}
// TestRefreshTokenSecurity tests security aspects of token refresh
func TestRefreshTokenSecurity(t *testing.T) {
setup := setupTestServer(t)
client := &http.Client{}
t.Run("RefreshToken_ReuseProtection", func(t *testing.T) {
// Login to get tokens
loginReq := map[string]string{
"login": "testuser@tests.bookhoard.internal",
"password": "Test@Pass123!",
}
loginBody, _ := json.Marshal(loginReq)
loginHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
loginHTTP.Header.Set("Content-Type", "application/json")
loginResp, err := client.Do(loginHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(loginResp.Body)
require.Equal(t, http.StatusOK, loginResp.StatusCode)
var loginResult map[string]interface{}
err = json.NewDecoder(loginResp.Body).Decode(&loginResult)
require.NoError(t, err)
refreshToken := loginResult["refresh_token"].(string)
// Use the refresh token first time
refreshReq := map[string]interface{}{
"refresh_token": refreshToken,
}
refreshBody, _ := json.Marshal(refreshReq)
refreshHTTP1, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
refreshHTTP1.Header.Set("Content-Type", "application/json")
refreshResp1, err := client.Do(refreshHTTP1)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(refreshResp1.Body)
assert.Equal(t, http.StatusOK, refreshResp1.StatusCode)
// Try to reuse the same refresh token (should fail if refresh token rotation is enabled)
refreshHTTP2, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
refreshHTTP2.Header.Set("Content-Type", "application/json")
refreshResp2, err := client.Do(refreshHTTP2)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(refreshResp2.Body)
// May return 401 if token reuse is detected, or 200 if not implemented
// Either is acceptable depending on security requirements
assert.True(t, refreshResp2.StatusCode == http.StatusOK || refreshResp2.StatusCode == http.StatusUnauthorized)
})
}
// TestRefreshTokenEdgeCases tests edge cases for token refresh
func TestRefreshTokenEdgeCases(t *testing.T) {
setup := setupTestServer(t)
client := &http.Client{}
t.Run("RefreshToken_EmptyStringToken", func(t *testing.T) {
req := map[string]interface{}{
"refresh_token": "",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("RefreshToken_NullToken", func(t *testing.T) {
req := map[string]interface{}{
"refresh_token": nil,
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("RefreshToken_ResponseStructure", func(t *testing.T) {
// Login to get tokens
loginReq := map[string]string{
"login": "testuser@tests.bookhoard.internal",
"password": "Test@Pass123!",
}
loginBody, _ := json.Marshal(loginReq)
loginHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
loginHTTP.Header.Set("Content-Type", "application/json")
loginResp, err := client.Do(loginHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(loginResp.Body)
require.Equal(t, http.StatusOK, loginResp.StatusCode)
var loginResult map[string]interface{}
err = json.NewDecoder(loginResp.Body).Decode(&loginResult)
require.NoError(t, err)
refreshToken := loginResult["refresh_token"].(string)
// Refresh the token
refreshReq := map[string]interface{}{
"refresh_token": refreshToken,
}
refreshBody, _ := json.Marshal(refreshReq)
refreshHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
refreshHTTP.Header.Set("Content-Type", "application/json")
refreshResp, err := client.Do(refreshHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(refreshResp.Body)
require.Equal(t, http.StatusOK, refreshResp.StatusCode)
var refreshResult map[string]interface{}
err = json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
require.NoError(t, err)
// Verify response structure
assert.Contains(t, refreshResult, "access_token")
assert.NotEmpty(t, refreshResult["access_token"])
// May or may not contain refresh_token (if rotation is enabled)
// Both are valid responses
})
t.Run("RefreshToken_TokenType", func(t *testing.T) {
// Login to get tokens
loginReq := map[string]string{
"login": "testuser@tests.bookhoard.internal",
"password": "Test@Pass123!",
}
loginBody, _ := json.Marshal(loginReq)
loginHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
loginHTTP.Header.Set("Content-Type", "application/json")
loginResp, err := client.Do(loginHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(loginResp.Body)
require.Equal(t, http.StatusOK, loginResp.StatusCode)
var loginResult map[string]interface{}
err = json.NewDecoder(loginResp.Body).Decode(&loginResult)
require.NoError(t, err)
refreshToken := loginResult["refresh_token"].(string)
// Refresh the token
refreshReq := map[string]interface{}{
"refresh_token": refreshToken,
}
refreshBody, _ := json.Marshal(refreshReq)
refreshHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
refreshHTTP.Header.Set("Content-Type", "application/json")
refreshResp, err := client.Do(refreshHTTP)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(refreshResp.Body)
require.Equal(t, http.StatusOK, refreshResp.StatusCode)
var refreshResult map[string]interface{}
err = json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
require.NoError(t, err)
// Verify access token is a string
accessToken, ok := refreshResult["access_token"].(string)
assert.True(t, ok, "access_token should be a string")
assert.NotEmpty(t, accessToken, "access_token should not be empty")
assert.Greater(t, len(accessToken), 20, "access_token should be a reasonably long JWT")
})
}