Files
user-system/internal/api/handler/sso_handler_test.go

856 lines
27 KiB
Go
Raw Normal View History

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)
}
}