test: add comprehensive test coverage for API endpoints and services
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
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) {
|
||||
t.Run("RefreshToken_MissingToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "not-a-valid-jwt-token",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_ExpiredToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// This would require an expired token - for now test with invalid token
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAwMDAwMDB9.expired",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_ValidToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// First, login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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", ts.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) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "some-token",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
// Don't set Content-Type
|
||||
|
||||
client := &http.Client{}
|
||||
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) {
|
||||
t.Run("RefreshToken_ReuseProtection", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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", ts.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", ts.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)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_TokenTampering", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get a valid token
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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)
|
||||
|
||||
// Tamper with the token by modifying a character
|
||||
if len(refreshToken) > 10 {
|
||||
tamperedToken := refreshToken[:5] + "X" + refreshToken[6:]
|
||||
|
||||
refreshReq := map[string]interface{}{
|
||||
"refresh_token": tamperedToken,
|
||||
}
|
||||
refreshBody, _ := json.Marshal(refreshReq)
|
||||
|
||||
refreshHTTP, _ := http.NewRequest("POST", ts.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.StatusUnauthorized, refreshResp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRefreshTokenEdgeCases tests edge cases for token refresh
|
||||
func TestRefreshTokenEdgeCases(t *testing.T) {
|
||||
t.Run("RefreshToken_EmptyStringToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": nil,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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", ts.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) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
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", ts.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")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user