package main import ( "bytes" "encoding/json" "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 resp.Body.Close() 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 resp.Body.Close() 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 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, _ := 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 refreshResp.Body.Close() assert.Equal(t, http.StatusOK, refreshResp.StatusCode) var refreshResult map[string]interface{} json.NewDecoder(refreshResp.Body).Decode(&refreshResult) 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 resp.Body.Close() 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 resp.Body.Close() // 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 loginResp.Body.Close() require.Equal(t, http.StatusOK, loginResp.StatusCode) var loginResult map[string]interface{} json.NewDecoder(loginResp.Body).Decode(&loginResult) 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 refreshResp1.Body.Close() 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 refreshResp2.Body.Close() // 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 resp.Body.Close() 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 resp.Body.Close() 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 loginResp.Body.Close() require.Equal(t, http.StatusOK, loginResp.StatusCode) var loginResult map[string]interface{} json.NewDecoder(loginResp.Body).Decode(&loginResult) 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 refreshResp.Body.Close() require.Equal(t, http.StatusOK, refreshResp.StatusCode) var refreshResult map[string]interface{} json.NewDecoder(refreshResp.Body).Decode(&refreshResult) // 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 loginResp.Body.Close() require.Equal(t, http.StatusOK, loginResp.StatusCode) var loginResult map[string]interface{} json.NewDecoder(loginResp.Body).Decode(&loginResult) 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 refreshResp.Body.Close() require.Equal(t, http.StatusOK, refreshResp.StatusCode) var refreshResult map[string]interface{} json.NewDecoder(refreshResp.Body).Decode(&refreshResult) // 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") }) }