package auth import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "nixcn-cms/service/service_auth" "nixcn-cms/testutil" ) func init() { gin.SetMode(gin.TestMode) } func setupAuthRouter(t *testing.T) *gin.Engine { t.Helper() testutil.Setup(t) testutil.SeedClient(t) r := gin.New() ApiHandler(r.Group("/auth")) return r } func postJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func getRequest(t *testing.T, r *gin.Engine, path string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func postWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder { t.Helper() var bodyReader io.Reader if body != nil { b, _ := json.Marshal(body) bodyReader = bytes.NewBuffer(b) } req := httptest.NewRequest(http.MethodPost, path, bodyReader) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func extractQueryParam(t *testing.T, rawURL, param string) string { t.Helper() u, err := url.Parse(rawURL) require.NoError(t, err) return u.Query().Get(param) } // ---- Magic ---- func TestMagicHandlerSuccess(t *testing.T) { r := setupAuthRouter(t) w := postJSON(t, r, "/auth/magic", service_auth.MagicData{ ClientId: testutil.TestClientID, RedirectUri: "http://localhost/callback", State: "s", Email: "handler@example.com", }) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) data, ok := resp["data"].(map[string]any) require.True(t, ok) assert.NotEmpty(t, data["uri"]) } func TestMagicHandlerInvalidJSON(t *testing.T) { r := setupAuthRouter(t) req := httptest.NewRequest(http.MethodPost, "/auth/magic", bytes.NewBufferString("{invalid")) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ---- Redirect ---- func TestRedirectHandlerMissingParams(t *testing.T) { r := setupAuthRouter(t) w := getRequest(t, r, "/auth/redirect") assert.Equal(t, http.StatusBadRequest, w.Code) } func TestRedirectHandlerInvalidCode(t *testing.T) { r := setupAuthRouter(t) w := getRequest(t, r, "/auth/redirect?client_id="+testutil.TestClientID+ "&redirect_uri=http://localhost/callback&code=bad-code&state=s") assert.Equal(t, http.StatusForbidden, w.Code) } // ---- Token ---- func TestTokenHandlerInvalidCode(t *testing.T) { r := setupAuthRouter(t) w := postJSON(t, r, "/auth/token", service_auth.TokenData{Code: "invalid"}) assert.Equal(t, http.StatusForbidden, w.Code) } // ---- Refresh ---- func TestRefreshHandlerInvalidToken(t *testing.T) { r := setupAuthRouter(t) w := postJSON(t, r, "/auth/refresh", service_auth.RefreshData{RefreshToken: "bad"}) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ---- Full flow: Magic → Redirect → Token → Refresh ---- func TestAuthFullFlow(t *testing.T) { r := setupAuthRouter(t) // 1. Magic magicW := postJSON(t, r, "/auth/magic", service_auth.MagicData{ ClientId: testutil.TestClientID, RedirectUri: "http://localhost/callback", State: "s", Email: "flow@example.com", }) require.Equal(t, http.StatusOK, magicW.Code) var magicResp map[string]any require.NoError(t, json.Unmarshal(magicW.Body.Bytes(), &magicResp)) dataMap := magicResp["data"].(map[string]any) rawURI := dataMap["uri"].(string) code := extractQueryParam(t, rawURI, "code") require.NotEmpty(t, code) // 2. Redirect → 302 redirectW := getRequest(t, r, "/auth/redirect?client_id="+testutil.TestClientID+ "&redirect_uri=http://localhost/callback&code="+code+"&state=s") require.Equal(t, http.StatusFound, redirectW.Code) location := redirectW.Header().Get("Location") require.NotEmpty(t, location) tokenCode := extractQueryParam(t, location, "code") require.NotEmpty(t, tokenCode) // 3. Token tokenW := postJSON(t, r, "/auth/token", service_auth.TokenData{Code: tokenCode}) require.Equal(t, http.StatusOK, tokenW.Code) var tokenResp map[string]any require.NoError(t, json.Unmarshal(tokenW.Body.Bytes(), &tokenResp)) tokenData := tokenResp["data"].(map[string]any) accessToken := tokenData["access_token"].(string) refreshToken := tokenData["refresh_token"].(string) // 4. Refresh refreshW := postJSON(t, r, "/auth/refresh", service_auth.RefreshData{ RefreshToken: refreshToken, }) require.Equal(t, http.StatusOK, refreshW.Code) var refreshResp map[string]any require.NoError(t, json.Unmarshal(refreshW.Body.Bytes(), &refreshResp)) refreshData := refreshResp["data"].(map[string]any) assert.NotEmpty(t, refreshData["access_token"]) assert.NotEqual(t, refreshToken, refreshData["refresh_token"]) _ = accessToken } // ---- Exchange ---- func TestExchangeHandlerNoAuth(t *testing.T) { r := setupAuthRouter(t) w := postJSON(t, r, "/auth/exchange", service_auth.ExchangeData{ ClientId: testutil.TestClientID, RedirectUri: "http://localhost/callback", State: "s", }) // No auth header → 401 assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestExchangeHandlerSuccess(t *testing.T) { r := setupAuthRouter(t) // Step 1: Magic (debug mode, returns redirect URI with code) magicW := postJSON(t, r, "/auth/magic", service_auth.MagicData{ ClientId: testutil.TestClientID, RedirectUri: "http://localhost/callback", State: "s", Email: "exchange@example.com", }) require.Equal(t, http.StatusOK, magicW.Code) var magicResp map[string]any require.NoError(t, json.Unmarshal(magicW.Body.Bytes(), &magicResp)) rawURI := magicResp["data"].(map[string]any)["uri"].(string) code := extractQueryParam(t, rawURI, "code") // Step 2: Redirect → produces token code redirectW := getRequest(t, r, "/auth/redirect?client_id="+testutil.TestClientID+ "&redirect_uri=http://localhost/callback&code="+code+"&state=s") require.Equal(t, http.StatusFound, redirectW.Code) location := redirectW.Header().Get("Location") tokenCode := extractQueryParam(t, location, "code") // Step 3: Token → get access token tokenW := postJSON(t, r, "/auth/token", service_auth.TokenData{Code: tokenCode}) require.Equal(t, http.StatusOK, tokenW.Code) var tokenResp map[string]any require.NoError(t, json.Unmarshal(tokenW.Body.Bytes(), &tokenResp)) accessToken := tokenResp["data"].(map[string]any)["access_token"].(string) // Step 4: Exchange (requires JWT Bearer token) w := postWithBearer(t, r, "/auth/exchange", accessToken, service_auth.ExchangeData{ ClientId: testutil.TestClientID, RedirectUri: "http://localhost/callback", State: "s", }) assert.Equal(t, http.StatusOK, w.Code) var exchangeResp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &exchangeResp)) exchangeData, ok := exchangeResp["data"].(map[string]any) require.True(t, ok) assert.NotEmpty(t, exchangeData["redirect_uri"]) }