package handler_test import ( "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/gin-gonic/gin" "github.com/user-management-system/internal/api/handler" "github.com/user-management-system/internal/auth" ) func doPostForm(targetURL, token string, data url.Values) (*http.Response, string) { var bodyReader io.Reader if data != nil { bodyReader = strings.NewReader(data.Encode()) } req, _ := http.NewRequest("POST", targetURL, bodyReader) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, _ := client.Do(req) bodyBytes, _ := io.ReadAll(resp.Body) resp.Body.Close() return resp, string(bodyBytes) } func setupSSOTestServer(t *testing.T) (*httptest.Server, func()) { t.Helper() gin.SetMode(gin.TestMode) engine := gin.New() engine.Use(gin.Recovery()) ssoManager := auth.NewSSOManager() clientsStore := auth.NewDefaultSSOClientsStore() clientsStore.RegisterClient(&auth.SSOClient{ ClientID: "test-client", ClientSecret: "test-secret", Name: "Test Client", RedirectURIs: []string{"http://localhost:8080/callback"}, }) ssoHandler := handler.NewSSOHandler(ssoManager, clientsStore) // Simple auth middleware for testing authMiddleware := func() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" || token == "Bearer " { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"}) return } c.Set("user_id", int64(1)) c.Set("username", "testuser") c.Next() } }() ssoGroup := engine.Group("/api/v1/sso") ssoGroup.Use(authMiddleware) { ssoGroup.GET("/authorize", ssoHandler.Authorize) ssoGroup.POST("/token", ssoHandler.Token) ssoGroup.POST("/introspect", ssoHandler.Introspect) ssoGroup.POST("/revoke", ssoHandler.Revoke) ssoGroup.GET("/userinfo", ssoHandler.UserInfo) } server := httptest.NewServer(engine) return server, func() { server.Close() } } func TestSSOHandler_Authorize_MissingParams(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doGet(server.URL+"/api/v1/sso/authorize", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Authorize_UnsupportedResponseType(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=unsupported", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Authorize_Unauthorized(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code", "") defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, resp.StatusCode) } } func TestSSOHandler_Authorize_CodeFlow(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code&state=xyz", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusFound { t.Fatalf("expected status %d (redirect), got %d", http.StatusFound, resp.StatusCode) } location := resp.Header.Get("Location") if location == "" { t.Fatal("expected redirect location") } if !strings.Contains(location, "code=") { t.Errorf("expected redirect with code, got %s", location) } if !strings.Contains(location, "state=xyz") { t.Errorf("expected redirect with state, got %s", location) } } func TestSSOHandler_Authorize_InvalidRedirectURI(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://evil.com/callback&response_type=code", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Authorize_TokenFlow(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=token&state=abc", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusFound { t.Fatalf("expected status %d (redirect), got %d", http.StatusFound, resp.StatusCode) } location := resp.Header.Get("Location") if location == "" { t.Fatal("expected redirect location") } if !strings.Contains(location, "access_token=") { t.Errorf("expected redirect with access_token, got %s", location) } } func TestSSOHandler_Token_MissingParams(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", nil) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Token_InvalidGrantType(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() formData := url.Values{} formData.Set("grant_type", "password") formData.Set("client_id", "test-client") formData.Set("client_secret", "test-secret") resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", formData) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Token_InvalidClient(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", "some-code") formData.Set("client_id", "invalid-client") formData.Set("client_secret", "wrong-secret") resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", formData) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d, body: %s", http.StatusUnauthorized, resp.StatusCode, body) } } func TestSSOHandler_Token_InvalidCode(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", "invalid-code") formData.Set("client_id", "test-client") formData.Set("client_secret", "test-secret") resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", formData) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d, body: %s", http.StatusUnauthorized, resp.StatusCode, body) } } func TestSSOHandler_Token_Success(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // First authorize to get a code authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer authResp.Body.Close() if authResp.StatusCode != http.StatusFound { t.Fatalf("expected authorize redirect, got %d", authResp.StatusCode) } location := authResp.Header.Get("Location") parsedURL, err := url.Parse(location) if err != nil { t.Fatalf("failed to parse redirect URL: %v", err) } code := parsedURL.Query().Get("code") if code == "" { t.Fatal("expected authorization code in redirect") } // Exchange code for token formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", code) formData.Set("client_id", "test-client") formData.Set("client_secret", "test-secret") resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", formData) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } var tokenResp handler.TokenResponse if err := json.Unmarshal([]byte(body), &tokenResp); err != nil { t.Fatalf("failed to parse token response: %v", err) } if tokenResp.AccessToken == "" { t.Errorf("expected access_token in response") } if tokenResp.TokenType != "Bearer" { t.Errorf("expected token_type Bearer, got %s", tokenResp.TokenType) } } func TestSSOHandler_Introspect_MissingToken(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/sso/introspect", "Bearer test-token", map[string]interface{}{}) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Introspect_InvalidToken(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/sso/introspect", "Bearer test-token", map[string]interface{}{ "token": "invalid-token", }) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } var result handler.IntrospectResponse if err := json.Unmarshal([]byte(body), &result); err != nil { t.Fatalf("failed to parse introspect response: %v", err) } if result.Active != false { t.Errorf("expected active=false for invalid token, got %v", result.Active) } } func TestSSOHandler_Introspect_ValidToken(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Authorize and get token authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer authResp.Body.Close() location := authResp.Header.Get("Location") parsedURL, _ := url.Parse(location) code := parsedURL.Query().Get("code") tokenForm := url.Values{} tokenForm.Set("grant_type", "authorization_code") tokenForm.Set("code", code) tokenForm.Set("client_id", "test-client") tokenForm.Set("client_secret", "test-secret") tokenResp, tokenBody := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", tokenForm) defer tokenResp.Body.Close() if tokenResp.StatusCode != http.StatusOK { t.Fatalf("token exchange failed: status=%d body=%s", tokenResp.StatusCode, tokenBody) } var tokenResult handler.TokenResponse if err := json.Unmarshal([]byte(tokenBody), &tokenResult); err != nil { t.Fatalf("failed to parse token response: %v", err) } // Introspect the token resp, body := doPost(server.URL+"/api/v1/sso/introspect", "Bearer test-token", map[string]interface{}{ "token": tokenResult.AccessToken, }) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } var result handler.IntrospectResponse if err := json.Unmarshal([]byte(body), &result); err != nil { t.Fatalf("failed to parse introspect response: %v", err) } if result.Active != true { t.Errorf("expected active=true for valid token, got %v", result.Active) } if result.UserID != 1 { t.Errorf("expected user_id=1, got %d", result.UserID) } } func TestSSOHandler_Revoke_MissingToken(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/sso/revoke", "Bearer test-token", map[string]interface{}{}) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Revoke_Success(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Authorize and get token authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer authResp.Body.Close() location := authResp.Header.Get("Location") parsedURL, _ := url.Parse(location) code := parsedURL.Query().Get("code") tokenForm := url.Values{} tokenForm.Set("grant_type", "authorization_code") tokenForm.Set("code", code) tokenForm.Set("client_id", "test-client") tokenForm.Set("client_secret", "test-secret") tokenResp, tokenBody := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", tokenForm) defer tokenResp.Body.Close() if tokenResp.StatusCode != http.StatusOK { t.Fatalf("token exchange failed: status=%d body=%s", tokenResp.StatusCode, tokenBody) } var tokenResult handler.TokenResponse if err := json.Unmarshal([]byte(tokenBody), &tokenResult); err != nil { t.Fatalf("failed to parse token response: %v", err) } // Revoke the token resp, body := doPost(server.URL+"/api/v1/sso/revoke", "Bearer test-token", map[string]interface{}{ "token": tokenResult.AccessToken, }) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } // Verify token is revoked introspectResp, introspectBody := doPost(server.URL+"/api/v1/sso/introspect", "Bearer test-token", map[string]interface{}{ "token": tokenResult.AccessToken, }) defer introspectResp.Body.Close() if introspectResp.StatusCode != http.StatusOK { t.Fatalf("introspect failed: status=%d body=%s", introspectResp.StatusCode, introspectBody) } var introspectResult handler.IntrospectResponse if err := json.Unmarshal([]byte(introspectBody), &introspectResult); err != nil { t.Fatalf("failed to parse introspect response: %v", err) } if introspectResult.Active != false { t.Errorf("expected active=false after revoke, got %v", introspectResult.Active) } } func TestSSOHandler_UserInfo_Unauthorized(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doGet(server.URL+"/api/v1/sso/userinfo", "") defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, resp.StatusCode) } } func TestSSOHandler_UserInfo_Success(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doGet(server.URL+"/api/v1/sso/userinfo", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } var result map[string]interface{} if err := json.Unmarshal([]byte(body), &result); err != nil { t.Fatalf("failed to parse response: %v", err) } if result["code"] != float64(0) { t.Errorf("expected code 0, got %v", result["code"]) } data, ok := result["data"].(map[string]interface{}) if !ok { t.Fatalf("expected data in response, got %s", body) } if data["user_id"] != float64(1) { t.Errorf("expected user_id=1, got %v", data["user_id"]) } if data["username"] != "testuser" { t.Errorf("expected username=testuser, got %v", data["username"]) } } func TestSSOHandler_Token_InvalidClientSecret(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Authorize to get a code authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer authResp.Body.Close() location := authResp.Header.Get("Location") parsedURL, _ := url.Parse(location) code := parsedURL.Query().Get("code") formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", code) formData.Set("client_id", "test-client") formData.Set("client_secret", "wrong-secret") resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", formData) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d, body: %s", http.StatusUnauthorized, resp.StatusCode, body) } } func TestSSOHandler_Authorize_MissingClientID(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doGet(server.URL+"/api/v1/sso/authorize?redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Introspect_FormData(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Test that introspect accepts form-encoded data formData := url.Values{} formData.Set("token", "some-token") req, _ := http.NewRequest("POST", server.URL+"/api/v1/sso/introspect", strings.NewReader(formData.Encode())) req.Header.Set("Authorization", "Bearer test-token") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := json.Marshal(resp.Body) t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, string(bodyBytes)) } } func TestSSOHandler_Token_FormData(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Authorize to get a code authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer authResp.Body.Close() location := authResp.Header.Get("Location") parsedURL, _ := url.Parse(location) code := parsedURL.Query().Get("code") // Test that token accepts form-encoded data formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", code) formData.Set("client_id", "test-client") formData.Set("client_secret", "test-secret") req, _ := http.NewRequest("POST", server.URL+"/api/v1/sso/token", strings.NewReader(formData.Encode())) req.Header.Set("Authorization", "Bearer test-token") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() bodyBytes, _ := json.Marshal(resp.Body) if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, string(bodyBytes)) } } func TestSSOHandler_Revoke_FormData(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() formData := url.Values{} formData.Set("token", "some-token") req, _ := http.NewRequest("POST", server.URL+"/api/v1/sso/revoke", strings.NewReader(formData.Encode())) req.Header.Set("Authorization", "Bearer test-token") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := json.Marshal(resp.Body) t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, string(bodyBytes)) } } func TestSSOHandler_Authorize_UnknownClientID(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, body := doGet(server.URL+"/api/v1/sso/authorize?client_id=unknown-client&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer resp.Body.Close() // When client is unknown, redirect_uri validation fails if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Token_WithoutAuth(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", "some-code") formData.Set("client_id", "test-client") formData.Set("client_secret", "test-secret") resp, _ := doPostForm(server.URL+"/api/v1/sso/token", "", formData) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, resp.StatusCode) } } func TestSSOHandler_UserInfo_WithoutAuth(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doGet(server.URL+"/api/v1/sso/userinfo", "") defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, resp.StatusCode) } } func TestSSOHandler_Introspect_WithoutAuth(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doPost(server.URL+"/api/v1/sso/introspect", "", map[string]interface{}{ "token": "some-token", }) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, resp.StatusCode) } } func TestSSOHandler_Revoke_WithoutAuth(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() resp, _ := doPost(server.URL+"/api/v1/sso/revoke", "", map[string]interface{}{ "token": "some-token", }) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, resp.StatusCode) } } func TestSSOHandler_Authorize_InvalidClientID(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Test with valid redirect URI but unknown client resp, body := doGet(server.URL+"/api/v1/sso/authorize?client_id=unknown&redirect_uri=http://localhost:8080/callback&response_type=code", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestSSOHandler_Token_MissingCode(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("client_id", "test-client") formData.Set("client_secret", "test-secret") resp, body := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", formData) defer resp.Body.Close() // Code is empty, so validate should fail if resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected status %d, got %d, body: %s", http.StatusUnauthorized, resp.StatusCode, body) } } func TestSSOHandler_FullFlow(t *testing.T) { server, cleanup := setupSSOTestServer(t) defer cleanup() // Step 1: Authorize authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code&state=my-state", "Bearer test-token") defer authResp.Body.Close() if authResp.StatusCode != http.StatusFound { t.Fatalf("authorize failed: status=%d", authResp.StatusCode) } location := authResp.Header.Get("Location") parsedURL, _ := url.Parse(location) code := parsedURL.Query().Get("code") state := parsedURL.Query().Get("state") if code == "" { t.Fatal("expected authorization code") } if state != "my-state" { t.Errorf("expected state=my-state, got %s", state) } // Step 2: Exchange code for token tokenForm := url.Values{} tokenForm.Set("grant_type", "authorization_code") tokenForm.Set("code", code) tokenForm.Set("client_id", "test-client") tokenForm.Set("client_secret", "test-secret") tokenResp, tokenBody := doPostForm(server.URL+"/api/v1/sso/token", "Bearer test-token", tokenForm) defer tokenResp.Body.Close() if tokenResp.StatusCode != http.StatusOK { t.Fatalf("token exchange failed: status=%d body=%s", tokenResp.StatusCode, tokenBody) } var tokenResult handler.TokenResponse if err := json.Unmarshal([]byte(tokenBody), &tokenResult); err != nil { t.Fatalf("failed to parse token response: %v", err) } if tokenResult.AccessToken == "" { t.Fatal("expected access_token") } // Step 3: Introspect token introspectResp, introspectBody := doPost(server.URL+"/api/v1/sso/introspect", "Bearer test-token", map[string]interface{}{ "token": tokenResult.AccessToken, }) defer introspectResp.Body.Close() if introspectResp.StatusCode != http.StatusOK { t.Fatalf("introspect failed: status=%d body=%s", introspectResp.StatusCode, introspectBody) } var introspectResult handler.IntrospectResponse if err := json.Unmarshal([]byte(introspectBody), &introspectResult); err != nil { t.Fatalf("failed to parse introspect response: %v", err) } if !introspectResult.Active { t.Error("expected token to be active") } if introspectResult.UserID != 1 { t.Errorf("expected user_id=1, got %d", introspectResult.UserID) } // Step 4: Get userinfo userinfoResp, userinfoBody := doGet(server.URL+"/api/v1/sso/userinfo", "Bearer test-token") defer userinfoResp.Body.Close() if userinfoResp.StatusCode != http.StatusOK { t.Fatalf("userinfo failed: status=%d body=%s", userinfoResp.StatusCode, userinfoBody) } var userinfoResult map[string]interface{} if err := json.Unmarshal([]byte(userinfoBody), &userinfoResult); err != nil { t.Fatalf("failed to parse userinfo response: %v", err) } userinfoData, ok := userinfoResult["data"].(map[string]interface{}) if !ok { t.Fatalf("expected userinfo data, got %s", userinfoBody) } if userinfoData["username"] != "testuser" { t.Errorf("expected username=testuser, got %v", userinfoData["username"]) } // Step 5: Revoke token revokeResp, revokeBody := doPost(server.URL+"/api/v1/sso/revoke", "Bearer test-token", map[string]interface{}{ "token": tokenResult.AccessToken, }) defer revokeResp.Body.Close() if revokeResp.StatusCode != http.StatusOK { t.Fatalf("revoke failed: status=%d body=%s", revokeResp.StatusCode, revokeBody) } // Step 6: Verify token is revoked finalIntrospectResp, finalIntrospectBody := doPost(server.URL+"/api/v1/sso/introspect", "Bearer test-token", map[string]interface{}{ "token": tokenResult.AccessToken, }) defer finalIntrospectResp.Body.Close() if finalIntrospectResp.StatusCode != http.StatusOK { t.Fatalf("final introspect failed: status=%d body=%s", finalIntrospectResp.StatusCode, finalIntrospectBody) } var finalResult handler.IntrospectResponse if err := json.Unmarshal([]byte(finalIntrospectBody), &finalResult); err != nil { t.Fatalf("failed to parse final introspect response: %v", err) } if finalResult.Active { t.Error("expected token to be inactive after revoke") } } func TestSSOHandler_Authorize_NoClientStore(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() ssoManager := auth.NewSSOManager() // Pass nil clientsStore ssoHandler := handler.NewSSOHandler(ssoManager, nil) authMiddleware := func() gin.HandlerFunc { return func(c *gin.Context) { c.Set("user_id", int64(1)) c.Set("username", "testuser") c.Next() } }() ssoGroup := engine.Group("/api/v1/sso") ssoGroup.Use(authMiddleware) { ssoGroup.GET("/authorize", ssoHandler.Authorize) } server := httptest.NewServer(engine) defer server.Close() // Without clients store, any redirect_uri should be accepted (or validation skipped) resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=any&redirect_uri=http://any.com/callback&response_type=code", "Bearer test-token") defer resp.Body.Close() if resp.StatusCode != http.StatusFound { t.Errorf("expected redirect when clientsStore is nil, got %d", resp.StatusCode) } }