From 8d35e824ae44fb66180c2459a9691d7643665e85 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Jul 2025 15:53:29 +0800 Subject: [PATCH] Remove deprecated test files and refactor OAuth response handling - Deleted obsolete test files for various OpenAPI components, including config_test.go, dsl_test.go, hello_test.go, oauth_test.go, oauth_token_test.go, and openapi_test.go, to streamline the codebase. - Refactored OAuth response handling by integrating response methods from the response package, ensuring consistent error and success responses across OAuth endpoints. - Enhanced error handling and response structure for improved clarity and maintainability, aligning with best practices for API responses. --- openapi/kb/collection.go | 67 +++++++- openapi/oauth.go | 153 +++++++++--------- openapi/{ => response}/response.go | 48 +++--- openapi/{ => tests}/config_test.go | 69 +++++++- openapi/{ => tests/dsl}/dsl_test.go | 126 ++++++++------- openapi/{ => tests/hello}/hello_test.go | 34 ++-- openapi/{ => tests/oauth}/oauth_test.go | 72 +++++---- .../oauth/token_test.go} | 125 +++++++------- openapi/tests/openapi_test.go | 41 +++++ .../testutils/testutils.go} | 127 ++++++++------- 10 files changed, 524 insertions(+), 338 deletions(-) rename openapi/{ => response}/response.go (88%) rename openapi/{ => tests}/config_test.go (85%) rename openapi/{ => tests/dsl}/dsl_test.go (82%) rename openapi/{ => tests/hello}/hello_test.go (85%) rename openapi/{ => tests/oauth}/oauth_test.go (89%) rename openapi/{oauth_token_test.go => tests/oauth/token_test.go} (85%) create mode 100644 openapi/tests/openapi_test.go rename openapi/{openapi_test.go => tests/testutils/testutils.go} (88%) diff --git a/openapi/kb/collection.go b/openapi/kb/collection.go index e6364114..cf0b223d 100644 --- a/openapi/kb/collection.go +++ b/openapi/kb/collection.go @@ -1,9 +1,11 @@ package kb import ( + "fmt" "net/http" "github.com/gin-gonic/gin" + "github.com/yaoapp/gou/graphrag/types" "github.com/yaoapp/yao/kb" ) @@ -11,8 +13,44 @@ import ( // CreateCollection creates a new collection func CreateCollection(c *gin.Context) { - // TODO: Implement create collection logic - c.JSON(http.StatusCreated, gin.H{"message": "Collection created"}) + var req CreateCollectionRequest + + // Parse and bind JSON request + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format: " + err.Error()}) + return + } + + // Validate request parameters + if err := validateCreateCollectionRequest(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Knowledge base not initialized"}) + return + } + + // Create CollectionConfig + collectionConfig := types.CollectionConfig{ + ID: req.ID, + Metadata: req.Metadata, + Config: req.Config, + } + + // Call the actual CreateCollection method + collectionID, err := kb.Instance.CreateCollection(c.Request.Context(), collectionConfig) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create collection: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "Collection created successfully", + "collection_id": collectionID, + }) } // RemoveCollection removes an existing collection @@ -36,3 +74,28 @@ func GetCollections(c *gin.Context) { } c.JSON(http.StatusOK, collections) } + +// CreateCollectionRequest represents the request structure for creating a collection +type CreateCollectionRequest struct { + ID string `json:"id" binding:"required"` + Metadata map[string]interface{} `json:"metadata"` + Config *types.CreateCollectionOptions `json:"config" binding:"required"` +} + +// validateCreateCollectionRequest validates the incoming request for creating a collection +func validateCreateCollectionRequest(req *CreateCollectionRequest) error { + if req.ID == "" { + return fmt.Errorf("id is required") + } + + if req.Config == nil { + return fmt.Errorf("config is required") + } + + // Validate CreateCollectionOptions + if err := req.Config.Validate(); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + + return nil +} diff --git a/openapi/oauth.go b/openapi/oauth.go index 5eddfa59..f10e466e 100644 --- a/openapi/oauth.go +++ b/openapi/oauth.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/yaoapp/yao/openapi/oauth" "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/openapi/response" ) // OAuth handlers @@ -73,7 +74,7 @@ func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) { // Parse and validate authorization request authReq, parseErr := openapi.parseAuthorizationRequest(c) if parseErr != nil { - openapi.respondWithAuthorizationError(c, authReq.RedirectURI, parseErr, authReq.State) + response.RespondWithAuthorizationError(c, authReq.RedirectURI, parseErr, authReq.State) return } @@ -81,18 +82,18 @@ func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) { authResp, err := openapi.OAuth.Authorize(c, authReq) if err != nil { // OAuth service returned an error - openapi.respondWithAuthorizationError(c, authReq.RedirectURI, ErrServerError, authReq.State) + response.RespondWithAuthorizationError(c, authReq.RedirectURI, response.ErrServerError, authReq.State) return } // Check if authorization response contains an error if authResp.Error != "" { // Convert OAuth service error to ErrorResponse - oauthError := &ErrorResponse{ + oauthError := &response.ErrorResponse{ Code: authResp.Error, ErrorDescription: authResp.ErrorDescription, } - openapi.respondWithAuthorizationError(c, authReq.RedirectURI, oauthError, authReq.State) + response.RespondWithAuthorizationError(c, authReq.RedirectURI, oauthError, authReq.State) return } @@ -114,7 +115,7 @@ func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) { } // Fallback: return JSON response if no redirect URI (should not happen with valid requests) - openapi.respondWithSuccess(c, StatusOK, authResp) + response.RespondWithSuccess(c, response.StatusOK, authResp) } // oauthToken handles token requests - RFC 6749 Section 3.2 @@ -123,7 +124,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) { // Validate grant type if grantType == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } @@ -141,7 +142,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) { openapi.handleTokenExchangeGrant(c) default: - openapi.respondWithSecureError(c, StatusBadRequest, ErrUnsupportedGrantType) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrUnsupportedGrantType) } } @@ -150,20 +151,20 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Extract client credentials from Basic Auth header or form parameters clientID, clientSecret := openapi.extractClientCredentials(c) if clientID == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient) return } // Validate client credentials using OAuth service oauthService, ok := openapi.OAuth.(*oauth.Service) if !ok { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient) return } clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) if err != nil { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient) return } @@ -179,13 +180,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Basic validation for authorization code grant if code == "" || redirectURI == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // Validate that client supports authorization code grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeAuthorizationCode) { - openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) + response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient) return } @@ -194,13 +195,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Basic validation for device code grant if code == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // Validate that client supports device code grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeDeviceCode) { - openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) + response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient) return } @@ -210,7 +211,7 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Validate that client supports client credentials grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeClientCredentials) { - openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) + response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient) return } } @@ -219,16 +220,16 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin token, err := openapi.OAuth.Token(c, grantType, code, clientID, codeVerifier) if err != nil { // Convert OAuth service error to token error response with security headers - if oauthErr, ok := err.(*ErrorResponse); ok { - openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) + if oauthErr, ok := err.(*response.ErrorResponse); ok { + response.RespondWithSecureError(c, response.StatusBadRequest, oauthErr) } else { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidGrant) } return } // Return successful token response with OAuth security headers (RFC 6749 Section 5.1: MUST set Cache-Control: no-store) - openapi.respondWithSecureSuccess(c, StatusOK, token) + response.RespondWithSecureSuccess(c, response.StatusOK, token) } // handleRefreshTokenGrant handles refresh token requests - RFC 6749 Section 6 @@ -236,26 +237,26 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) { // Extract client credentials from Basic Auth header or form parameters clientID, clientSecret := openapi.extractClientCredentials(c) if clientID == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient) return } // Validate client credentials using OAuth service oauthService, ok := openapi.OAuth.(*oauth.Service) if !ok { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient) return } clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) if err != nil { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient) return } // Validate that client supports refresh token grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeRefreshToken) { - openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) + response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient) return } @@ -264,7 +265,7 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) { // Basic validation if refreshToken == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } @@ -277,16 +278,16 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) { } if err != nil { // Convert OAuth service error to token error response with security headers - if oauthErr, ok := err.(*ErrorResponse); ok { - openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) + if oauthErr, ok := err.(*response.ErrorResponse); ok { + response.RespondWithSecureError(c, response.StatusBadRequest, oauthErr) } else { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidGrant) } return } // Return successful refresh token response with security headers - openapi.respondWithSecureSuccess(c, StatusOK, refreshResponse) + response.RespondWithSecureSuccess(c, response.StatusOK, refreshResponse) } // handleTokenExchangeGrant handles token exchange requests - RFC 8693 @@ -298,7 +299,7 @@ func (openapi *OpenAPI) handleTokenExchangeGrant(c *gin.Context) { // Basic validation if subjectToken == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } @@ -306,16 +307,16 @@ func (openapi *OpenAPI) handleTokenExchangeGrant(c *gin.Context) { exchangeResponse, err := openapi.OAuth.TokenExchange(c, subjectToken, subjectTokenType, audience, scope) if err != nil { // Convert OAuth service error to token error response with security headers - if oauthErr, ok := err.(*ErrorResponse); ok { - openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) + if oauthErr, ok := err.(*response.ErrorResponse); ok { + response.RespondWithSecureError(c, response.StatusBadRequest, oauthErr) } else { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidGrant) } return } // Return successful token exchange response with security headers - openapi.respondWithSecureSuccess(c, StatusOK, exchangeResponse) + response.RespondWithSecureSuccess(c, response.StatusOK, exchangeResponse) } // oauthRevoke handles token revocation - RFC 7009 @@ -324,7 +325,7 @@ func (openapi *OpenAPI) oauthRevoke(c *gin.Context) { tokenTypeHint := c.PostForm("token_type_hint") // Optional hint about token type if token == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } @@ -333,14 +334,14 @@ func (openapi *OpenAPI) oauthRevoke(c *gin.Context) { if err != nil { // OAuth spec requires returning 200 even for invalid tokens to prevent information leakage // Only return error for server errors - if oauthErr, ok := err.(*ErrorResponse); ok && oauthErr.Code == ErrServerError.Code { - openapi.respondWithError(c, StatusInternalServerError, ErrServerError) + if oauthErr, ok := err.(*response.ErrorResponse); ok && oauthErr.Code == response.ErrServerError.Code { + response.RespondWithError(c, response.StatusInternalServerError, response.ErrServerError) return } } // RFC 7009: Return 200 OK for successful revocation (or invalid tokens) - c.Status(StatusOK) + c.Status(response.StatusOK) } // oauthIntrospect handles token introspection - RFC 7662 @@ -348,7 +349,7 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) { token := c.PostForm("token") if token == "" { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } @@ -356,15 +357,15 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) { introspectionResult, err := openapi.OAuth.Introspect(c, token) if err != nil { // Return inactive token response on error (RFC 7662) with security headers - response := &TokenIntrospectionResponse{ + tokenResponse := &response.TokenIntrospectionResponse{ Active: false, } - openapi.respondWithSecureSuccess(c, StatusOK, response) + response.RespondWithSecureSuccess(c, response.StatusOK, tokenResponse) return } // Convert OAuth service response to API response format - response := &TokenIntrospectionResponse{ + tokenResponse := &response.TokenIntrospectionResponse{ Active: introspectionResult.Active, Scope: introspectionResult.Scope, ClientID: introspectionResult.ClientID, @@ -376,19 +377,19 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) { Audience: introspectionResult.Audience, } - openapi.respondWithSecureSuccess(c, StatusOK, response) + response.RespondWithSecureSuccess(c, response.StatusOK, tokenResponse) } // oauthJWKS returns JSON Web Key Set - RFC 7517 func (openapi *OpenAPI) oauthJWKS(c *gin.Context) { jwks, err := openapi.OAuth.JWKS(c) if err != nil { - openapi.respondWithError(c, StatusInternalServerError, ErrServerError) + response.RespondWithError(c, response.StatusInternalServerError, response.ErrServerError) return } // RFC 7517 compliance: Return JWKS directly with security headers - openapi.respondWithSecureSuccess(c, StatusOK, jwks) + response.RespondWithSecureSuccess(c, response.StatusOK, jwks) } // oauthUserInfo returns user information - OpenID Connect Core 1.0 @@ -396,39 +397,39 @@ func (openapi *OpenAPI) oauthUserInfo(c *gin.Context) { // Check for Bearer token in Authorization header authHeader := c.GetHeader("Authorization") if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " { - openapi.respondWithError(c, StatusUnauthorized, ErrInvalidToken) + response.RespondWithError(c, response.StatusUnauthorized, response.ErrInvalidToken) return } // TODO: Implement user info retrieval - openapi.respondWithError(c, StatusNotImplemented, ErrServerError) + response.RespondWithError(c, response.StatusNotImplemented, response.ErrServerError) } // OAuth Extended Endpoints Implementation // oauthRegister handles dynamic client registration - RFC 7591 func (openapi *OpenAPI) oauthRegister(c *gin.Context) { - var req DynamicClientRegistrationRequest + var req response.DynamicClientRegistrationRequest if err := c.ShouldBindJSON(&req); err != nil { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClientMetadata) return } // Basic validation if len(req.RedirectURIs) == 0 { - openapi.respondWithSecureError(c, StatusBadRequest, ErrMissingRedirectURI) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrMissingRedirectURI) return } res, err := openapi.OAuth.DynamicClientRegistration(c, &req) if err != nil { - openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata) + response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClientMetadata) return } // Return the authorization response with security headers (RFC 7591 compliant, contains client credentials) - openapi.respondWithSecureSuccess(c, StatusCreated, res) + response.RespondWithSecureSuccess(c, response.StatusCreated, res) } // extractClientCredentials extracts client ID and secret from Basic Auth header or form parameters @@ -479,12 +480,12 @@ func (openapi *OpenAPI) oauthGetClient(c *gin.Context) { clientID := c.Param("client_id") if clientID == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // TODO: Implement client retrieval logic - openapi.respondWithError(c, StatusNotFound, ErrInvalidClient) + response.RespondWithError(c, response.StatusNotFound, response.ErrInvalidClient) } // oauthUpdateClient updates client configuration - RFC 7592 @@ -492,18 +493,18 @@ func (openapi *OpenAPI) oauthUpdateClient(c *gin.Context) { clientID := c.Param("client_id") if clientID == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } - var req DynamicClientRegistrationRequest + var req response.DynamicClientRegistrationRequest if err := c.ShouldBindJSON(&req); err != nil { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidClientMetadata) return } // TODO: Implement client update logic - openapi.respondWithError(c, StatusNotImplemented, ErrServerError) + response.RespondWithError(c, response.StatusNotImplemented, response.ErrServerError) } // oauthDeleteClient deletes client configuration - RFC 7592 @@ -511,12 +512,12 @@ func (openapi *OpenAPI) oauthDeleteClient(c *gin.Context) { clientID := c.Param("client_id") if clientID == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // TODO: Implement client deletion logic - c.Status(StatusNoContent) + c.Status(response.StatusNoContent) } // oauthDeviceAuthorization handles device authorization - RFC 8628 @@ -524,12 +525,12 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) { clientID := c.PostForm("client_id") if clientID == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // TODO: Implement device authorization logic - response := &DeviceAuthorizationResponse{ + deviceResponse := &response.DeviceAuthorizationResponse{ DeviceCode: "generated-device-code", UserCode: "USER-CODE", VerificationURI: "https://example.com/device", @@ -537,31 +538,31 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) { Interval: 5, // 5 seconds } - openapi.respondWithSuccess(c, StatusOK, response) + response.RespondWithSuccess(c, response.StatusOK, deviceResponse) } // oauthPushedAuthorizationRequest handles PAR - RFC 9126 func (openapi *OpenAPI) oauthPushedAuthorizationRequest(c *gin.Context) { - var req PushedAuthorizationRequest + var req response.PushedAuthorizationRequest if err := c.ShouldBind(&req); err != nil { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // Basic validation if req.ClientID == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // TODO: Implement PAR logic - response := &PushedAuthorizationResponse{ + parResponse := &response.PushedAuthorizationResponse{ RequestURI: "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2", ExpiresIn: 60, // 60 seconds } - openapi.respondWithSuccess(c, StatusCreated, response) + response.RespondWithSuccess(c, response.StatusCreated, parResponse) } // oauthTokenExchange handles token exchange - RFC 8693 @@ -569,29 +570,29 @@ func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) { grantType := c.PostForm("grant_type") if grantType != types.GrantTypeTokenExchange { - openapi.respondWithError(c, StatusBadRequest, ErrUnsupportedGrantType) + response.RespondWithError(c, response.StatusBadRequest, response.ErrUnsupportedGrantType) return } subjectToken := c.PostForm("subject_token") if subjectToken == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest) return } // TODO: Implement token exchange logic - response := &TokenExchangeResponse{ + exchangeResponse := &response.TokenExchangeResponse{ AccessToken: "exchanged-access-token", IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", TokenType: types.TokenTypeBearer, ExpiresIn: 3600, // 1 hour } - openapi.respondWithSuccess(c, StatusOK, response) + response.RespondWithSuccess(c, response.StatusOK, exchangeResponse) } // parseAuthorizationRequest parses and validates authorization request parameters -func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*types.AuthorizationRequest, *ErrorResponse) { +func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*types.AuthorizationRequest, *response.ErrorResponse) { // Parse authorization request parameters from both GET (query) and POST (form) methods authReq := &types.AuthorizationRequest{ ClientID: openapi.getParam(c, "client_id"), @@ -607,12 +608,12 @@ func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*types.Author // Basic validation if authReq.ClientID == "" { - return authReq, ErrInvalidRequest + return authReq, response.ErrInvalidRequest } // Validate response_type parameter - RFC 6749 Section 3.1.1 if authReq.ResponseType == "" { - return authReq, ErrInvalidRequest + return authReq, response.ErrInvalidRequest } // Check supported response types @@ -621,9 +622,9 @@ func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*types.Author // Authorization code flow - supported case types.ResponseTypeToken: // Implicit flow - deprecated in OAuth 2.1, return error - return authReq, ErrUnsupportedResponseType + return authReq, response.ErrUnsupportedResponseType default: - return authReq, ErrUnsupportedResponseType + return authReq, response.ErrUnsupportedResponseType } return authReq, nil diff --git a/openapi/response.go b/openapi/response/response.go similarity index 88% rename from openapi/response.go rename to openapi/response/response.go index 0d1eadc3..c6168a04 100644 --- a/openapi/response.go +++ b/openapi/response/response.go @@ -1,4 +1,4 @@ -package openapi +package response import ( "net/http" @@ -147,7 +147,7 @@ const ( // setOAuthSecurityHeaders sets standard OAuth 2.0/2.1 security headers // These headers are required by OAuth 2.1 specification for enhanced security -func (openapi *OpenAPI) setOAuthSecurityHeaders(c *gin.Context) { +func SetOAuthSecurityHeaders(c *gin.Context) { c.Header("Cache-Control", "no-store") c.Header("Pragma", "no-cache") c.Header("X-Content-Type-Options", "nosniff") @@ -156,30 +156,30 @@ func (openapi *OpenAPI) setOAuthSecurityHeaders(c *gin.Context) { } // setJSONContentType sets JSON content type header for OAuth responses -func (openapi *OpenAPI) setJSONContentType(c *gin.Context) { +func SetJSONContentType(c *gin.Context) { c.Header("Content-Type", "application/json") } -// respondWithSuccess sends a successful response (no wrapper, direct data) -func (openapi *OpenAPI) respondWithSuccess(c *gin.Context, statusCode int, data interface{}) { - openapi.setJSONContentType(c) +// RespondWithSuccess sends a successful response (no wrapper, direct data) +func RespondWithSuccess(c *gin.Context, statusCode int, data interface{}) { + SetJSONContentType(c) c.JSON(statusCode, data) } -// respondWithError sends an error response (no wrapper, direct error) -func (openapi *OpenAPI) respondWithError(c *gin.Context, statusCode int, err *ErrorResponse) { - openapi.setJSONContentType(c) +// RespondWithError sends an error response (no wrapper, direct error) +func RespondWithError(c *gin.Context, statusCode int, err *ErrorResponse) { + SetJSONContentType(c) // Add WWW-Authenticate header for 401 responses if statusCode == StatusUnauthorized { - openapi.addWWWAuthenticateHeader(c, err) + AddWWWAuthenticateHeader(c, err) } c.JSON(statusCode, err) } -// respondWithAuthorizationError sends an authorization endpoint error via redirect -func (openapi *OpenAPI) respondWithAuthorizationError(c *gin.Context, redirectURI string, err *ErrorResponse, state string) { +// RespondWithAuthorizationError sends an authorization endpoint error via redirect +func RespondWithAuthorizationError(c *gin.Context, redirectURI string, err *ErrorResponse, state string) { // Build error redirect URL redirectURL := redirectURI if redirectURL != "" { @@ -204,11 +204,11 @@ func (openapi *OpenAPI) respondWithAuthorizationError(c *gin.Context, redirectUR } // Fallback to JSON error response if no redirect URI - openapi.respondWithError(c, StatusBadRequest, err) + RespondWithError(c, StatusBadRequest, err) } -// addWWWAuthenticateHeader adds appropriate WWW-Authenticate header -func (openapi *OpenAPI) addWWWAuthenticateHeader(c *gin.Context, err *ErrorResponse) { +// AddWWWAuthenticateHeader adds appropriate WWW-Authenticate header +func AddWWWAuthenticateHeader(c *gin.Context, err *ErrorResponse) { challenge := &WWWAuthenticateChallenge{ Scheme: types.WWWAuthenticateSchemeBearer, Realm: "OAuth", @@ -238,21 +238,21 @@ func (openapi *OpenAPI) addWWWAuthenticateHeader(c *gin.Context, err *ErrorRespo c.Header("WWW-Authenticate", headerValue) } -// respondWithSecureSuccess sends a successful response with OAuth security headers (for sensitive endpoints) -func (openapi *OpenAPI) respondWithSecureSuccess(c *gin.Context, statusCode int, data interface{}) { - openapi.setOAuthSecurityHeaders(c) - openapi.setJSONContentType(c) +// RespondWithSecureSuccess sends a successful response with OAuth security headers (for sensitive endpoints) +func RespondWithSecureSuccess(c *gin.Context, statusCode int, data interface{}) { + SetOAuthSecurityHeaders(c) + SetJSONContentType(c) c.JSON(statusCode, data) } -// respondWithSecureError sends an error response with OAuth security headers (for sensitive endpoints) -func (openapi *OpenAPI) respondWithSecureError(c *gin.Context, statusCode int, err *ErrorResponse) { - openapi.setOAuthSecurityHeaders(c) - openapi.setJSONContentType(c) +// RespondWithSecureError sends an error response with OAuth security headers (for sensitive endpoints) +func RespondWithSecureError(c *gin.Context, statusCode int, err *ErrorResponse) { + SetOAuthSecurityHeaders(c) + SetJSONContentType(c) // Add WWW-Authenticate header for 401 responses if statusCode == StatusUnauthorized { - openapi.addWWWAuthenticateHeader(c, err) + AddWWWAuthenticateHeader(c, err) } c.JSON(statusCode, err) diff --git a/openapi/config_test.go b/openapi/tests/config_test.go similarity index 85% rename from openapi/config_test.go rename to openapi/tests/config_test.go index f2ff62cf..f074a482 100644 --- a/openapi/config_test.go +++ b/openapi/tests/config_test.go @@ -1,11 +1,14 @@ -package openapi +package openapi_test import ( + "path/filepath" + "strings" "testing" "time" jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/openapi" "github.com/yaoapp/yao/openapi/oauth/types" ) @@ -37,7 +40,7 @@ func TestConfigUnmarshalJSON_TimeParsingCorrect(t *testing.T) { } }` - var config Config + var config openapi.Config err := jsoniter.Unmarshal([]byte(jsonData), &config) assert.NoError(t, err, "JSON unmarshaling should succeed") @@ -111,11 +114,11 @@ func TestFormatDuration(t *testing.T) { func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) { // Create a config with duration fields - originalConfig := &Config{ + originalConfig := &openapi.Config{ BaseURL: "/v1", Store: "__yao.oauth.store", Cache: "__yao.oauth.cache", - OAuth: &OAuth{ + OAuth: &openapi.OAuth{ IssuerURL: "https://localhost:5099", Signing: types.SigningConfig{ SigningCertPath: "/path/to/cert.pem", @@ -145,7 +148,7 @@ func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) { assert.NoError(t, err, "Marshal should succeed") // Unmarshal back to config - var unmarshaledConfig Config + var unmarshaledConfig openapi.Config err = jsoniter.Unmarshal(jsonData, &unmarshaledConfig) assert.NoError(t, err, "Unmarshal should succeed") @@ -178,11 +181,11 @@ func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) { // TestConfigJSONOutputDemo demonstrates the human-readable JSON output format func TestConfigJSONOutputDemo(t *testing.T) { - config := &Config{ + config := &openapi.Config{ BaseURL: "/v1", Store: "__yao.oauth.store", Cache: "__yao.oauth.cache", - OAuth: &OAuth{ + OAuth: &openapi.OAuth{ IssuerURL: "https://localhost:5099", Signing: types.SigningConfig{ SigningCertPath: "openapi/certs/signing-cert.pem", @@ -350,3 +353,55 @@ func TestCertificatePathConversion(t *testing.T) { } }) } + +// parseDuration parses a time duration string (e.g., "24h", "1h", "10m") into time.Duration +func parseDuration(durationStr string) (time.Duration, error) { + if durationStr == "" || durationStr == "0" || durationStr == "0s" { + return 0, nil + } + return time.ParseDuration(durationStr) +} + +// formatDuration converts time.Duration to human-readable string format +func formatDuration(duration time.Duration) string { + if duration == 0 { + return "0s" + } + return duration.String() +} + +// convertRelativeToAbsolutePath converts relative certificate path to absolute path +func convertRelativeToAbsolutePath(relativePath, rootPath string) string { + if relativePath == "" { + return "" + } + // If already absolute path, return as is + if filepath.IsAbs(relativePath) { + return relativePath + } + // Convert relative path to absolute: Root + "openapi" + "certs" + relativePath + return filepath.Join(rootPath, "openapi", "certs", relativePath) +} + +// convertAbsoluteToRelativePath converts absolute certificate path to relative path +func convertAbsoluteToRelativePath(absolutePath, rootPath string) string { + if absolutePath == "" { + return "" + } + // If not absolute path, return as is + if !filepath.IsAbs(absolutePath) { + return absolutePath + } + + // Remove Root + "openapi" + "certs" prefix + certBasePath := filepath.Join(rootPath, "openapi", "certs") + if strings.HasPrefix(absolutePath, certBasePath) { + relativePath := strings.TrimPrefix(absolutePath, certBasePath) + // Remove leading separator + relativePath = strings.TrimPrefix(relativePath, string(filepath.Separator)) + return relativePath + } + + // If path doesn't match expected pattern, return as is + return absolutePath +} diff --git a/openapi/dsl_test.go b/openapi/tests/dsl/dsl_test.go similarity index 82% rename from openapi/dsl_test.go rename to openapi/tests/dsl/dsl_test.go index 4e6c9b94..cc6805ee 100644 --- a/openapi/dsl_test.go +++ b/openapi/tests/dsl/dsl_test.go @@ -1,4 +1,4 @@ -package openapi +package openapi_test import ( "bytes" @@ -10,23 +10,25 @@ import ( "github.com/stretchr/testify/assert" "github.com/yaoapp/yao/dsl/types" + "github.com/yaoapp/yao/openapi" + "github.com/yaoapp/yao/openapi/tests/testutils" ) // TestDSLCreate tests the DSL creation endpoint func TestDSLCreate(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register test client and get token - client := RegisterTestClient(t, "DSL Create Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Create Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") // Generate unique test ID testID := fmt.Sprintf("test_model_%d", time.Now().UnixNano()) @@ -51,7 +53,7 @@ func TestDSLCreate(t *testing.T) { for _, store := range stores { t.Run(fmt.Sprintf("CreateModel_%s", store), func(t *testing.T) { - // Prepare request body + // testutils.Prepare request body createData := map[string]interface{}{ "id": testID + "_" + store, "source": modelSource, @@ -89,17 +91,17 @@ func TestDSLCreate(t *testing.T) { // TestDSLInspect tests the DSL inspection endpoint func TestDSLInspect(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL Inspect Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Inspect Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") testID := fmt.Sprintf("test_inspect_%d", time.Now().UnixNano()) modelSource := fmt.Sprintf(`{ @@ -162,17 +164,17 @@ func TestDSLInspect(t *testing.T) { // TestDSLSource tests the DSL source retrieval endpoint func TestDSLSource(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL Source Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Source Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") testID := fmt.Sprintf("test_source_%d", time.Now().UnixNano()) modelSource := fmt.Sprintf(`{ @@ -230,17 +232,17 @@ func TestDSLSource(t *testing.T) { // TestDSLList tests the DSL listing endpoint func TestDSLList(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL List Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL List Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") // Create multiple test models testTag := fmt.Sprintf("test_list_%d", time.Now().UnixNano()) @@ -317,17 +319,17 @@ func TestDSLList(t *testing.T) { // TestDSLUpdate tests the DSL update endpoint func TestDSLUpdate(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL Update Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Update Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") testID := fmt.Sprintf("test_update_%d", time.Now().UnixNano()) @@ -424,17 +426,17 @@ func TestDSLUpdate(t *testing.T) { // TestDSLExists tests the DSL existence check endpoint func TestDSLExists(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL Exists Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Exists Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") testID := fmt.Sprintf("test_exists_%d", time.Now().UnixNano()) nonExistentID := fmt.Sprintf("non_existent_%d", time.Now().UnixNano()) @@ -500,17 +502,17 @@ func TestDSLExists(t *testing.T) { // TestDSLDelete tests the DSL deletion endpoint func TestDSLDelete(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL Delete Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Delete Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") testID := fmt.Sprintf("test_delete_%d", time.Now().UnixNano()) @@ -576,17 +578,17 @@ func TestDSLDelete(t *testing.T) { // TestDSLValidate tests the DSL validation endpoint func TestDSLValidate(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "DSL Validate Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + client := testutils.RegisterTestClient(t, "DSL Validate Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") tests := []struct { name string @@ -662,12 +664,12 @@ func TestDSLValidate(t *testing.T) { // TestDSLUnauthorized tests that endpoints return 401 when not authenticated func TestDSLUnauthorized(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } endpoints := []struct { diff --git a/openapi/hello_test.go b/openapi/tests/hello/hello_test.go similarity index 85% rename from openapi/hello_test.go rename to openapi/tests/hello/hello_test.go index 11ff2074..f16b4bcc 100644 --- a/openapi/hello_test.go +++ b/openapi/tests/hello/hello_test.go @@ -1,4 +1,4 @@ -package openapi +package openapi_test import ( "encoding/json" @@ -6,17 +6,19 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/openapi" + "github.com/yaoapp/yao/openapi/tests/testutils" "github.com/yaoapp/yao/share" ) func TestHelloWorldPublic(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } tests := []struct { @@ -79,21 +81,21 @@ func TestHelloWorldPublic(t *testing.T) { } func TestHelloWorldProtected(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register a test client for authentication - client := RegisterTestClient(t, "Hello World Protected Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Hello World Protected Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) // Obtain access token for authentication - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") tests := []struct { name string @@ -164,13 +166,13 @@ func TestHelloWorldProtected(t *testing.T) { } func TestHelloWorldProtectedUnauthorized(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } tests := []struct { diff --git a/openapi/oauth_test.go b/openapi/tests/oauth/oauth_test.go similarity index 89% rename from openapi/oauth_test.go rename to openapi/tests/oauth/oauth_test.go index c8cf260f..a765a67e 100644 --- a/openapi/oauth_test.go +++ b/openapi/tests/oauth/oauth_test.go @@ -1,4 +1,4 @@ -package openapi +package openapi_test import ( "bytes" @@ -9,32 +9,34 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/openapi" "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/openapi/tests/testutils" ) func TestOAuthRegister(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() - // Debug: Check if Server is properly initialized - if Server == nil { - t.Fatal("OpenAPI Server is nil") + // Debug: Check if openapi.Server is properly initialized + if openapi.Server == nil { + t.Fatal("OpenAPI openapi.Server is nil") } - if Server.Config == nil { - t.Fatal("OpenAPI Server.Config is nil") + if openapi.Server.Config == nil { + t.Fatal("OpenAPI openapi.Server.Config is nil") } - if Server.OAuth == nil { - t.Fatal("OpenAPI Server.OAuth is nil") + if openapi.Server.OAuth == nil { + t.Fatal("OpenAPI openapi.Server.OAuth is nil") } - t.Logf("Server initialized with BaseURL: %s", Server.Config.BaseURL) + t.Logf("openapi.Server initialized with BaseURL: %s", openapi.Server.Config.BaseURL) // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } endpoint := serverURL + baseURL + "/oauth/register" @@ -108,24 +110,24 @@ func TestOAuthRegister(t *testing.T) { assert.Equal(t, req.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs) // Verify that default values were applied when not specified in request - assert.NotEmpty(t, response.DynamicClientRegistrationRequest.GrantTypes, "Server should apply default grant types") - assert.NotEmpty(t, response.DynamicClientRegistrationRequest.ResponseTypes, "Server should apply default response types") - assert.Equal(t, "web", response.DynamicClientRegistrationRequest.ApplicationType, "Server should apply default application type") - assert.Equal(t, "client_secret_basic", response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod, "Server should apply default auth method") + assert.NotEmpty(t, response.DynamicClientRegistrationRequest.GrantTypes, "openapi.Server should apply default grant types") + assert.NotEmpty(t, response.DynamicClientRegistrationRequest.ResponseTypes, "openapi.Server should apply default response types") + assert.Equal(t, "web", response.DynamicClientRegistrationRequest.ApplicationType, "openapi.Server should apply default application type") + assert.Equal(t, "client_secret_basic", response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod, "openapi.Server should apply default auth method") } }) } func TestOAuthAuthorize(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Register a test client for realistic testing - testClient := RegisterTestClient(t, "OAuth Test Client", []string{"http://localhost/callback"}) - defer CleanupTestClient(t, testClient.ClientID) + testClient := testutils.RegisterTestClient(t, "OAuth Test Client", []string{"http://localhost/callback"}) + defer testutils.CleanupTestClient(t, testClient.ClientID) - // Prepare test data - endpoint := serverURL + Server.Config.BaseURL + "/oauth/authorize" + // testutils.Prepare test data + endpoint := serverURL + openapi.Server.Config.BaseURL + "/oauth/authorize" t.Logf("Testing authorize endpoint: %s", endpoint) t.Run("Valid Authorization Request", func(t *testing.T) { @@ -324,28 +326,28 @@ func TestOAuthAuthorize(t *testing.T) { } func TestOAuthJWKS(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() - // Debug: Check if Server is properly initialized - if Server == nil { - t.Fatal("OpenAPI Server is nil") + // Debug: Check if openapi.Server is properly initialized + if openapi.Server == nil { + t.Fatal("OpenAPI openapi.Server is nil") } - if Server.Config == nil { - t.Fatal("OpenAPI Server.Config is nil") + if openapi.Server.Config == nil { + t.Fatal("OpenAPI openapi.Server.Config is nil") } - if Server.OAuth == nil { - t.Fatal("OpenAPI Server.OAuth is nil") + if openapi.Server.OAuth == nil { + t.Fatal("OpenAPI openapi.Server.OAuth is nil") } - t.Logf("Server initialized with BaseURL: %s", Server.Config.BaseURL) + t.Logf("openapi.Server initialized with BaseURL: %s", openapi.Server.Config.BaseURL) // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } endpoint := serverURL + baseURL + "/oauth/jwks" diff --git a/openapi/oauth_token_test.go b/openapi/tests/oauth/token_test.go similarity index 85% rename from openapi/oauth_token_test.go rename to openapi/tests/oauth/token_test.go index 1b780a0e..c2e20426 100644 --- a/openapi/oauth_token_test.go +++ b/openapi/tests/oauth/token_test.go @@ -1,4 +1,4 @@ -package openapi +package openapi_test import ( "bytes" @@ -9,29 +9,32 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/openapi" "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/openapi/response" + "github.com/yaoapp/yao/openapi/tests/testutils" ) func TestOAuthToken_AuthorizationCode(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register a test client - client := RegisterTestClient(t, "Token Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Token Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) // Obtain authorization code dynamically - authInfo := ObtainAuthorizationCode(t, serverURL, client.ClientID, "https://localhost/callback", "openid profile") + authInfo := testutils.ObtainAuthorizationCode(t, serverURL, client.ClientID, "https://localhost/callback", "openid profile") // Test authorization code grant t.Run("Valid Authorization Code Grant", func(t *testing.T) { - // Prepare token request with PKCE code verifier + // testutils.Prepare token request with PKCE code verifier data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("code", authInfo.Code) @@ -77,7 +80,7 @@ func TestOAuthToken_AuthorizationCode(t *testing.T) { t.Run("Invalid Authorization Code", func(t *testing.T) { // Test with invalid authorization code - should return error - // Prepare token request with invalid code + // testutils.Prepare token request with invalid code data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("code", "invalid-code") @@ -105,7 +108,7 @@ func TestOAuthToken_AuthorizationCode(t *testing.T) { }) t.Run("Missing Required Parameters", func(t *testing.T) { - // Prepare token request missing redirect_uri + // testutils.Prepare token request missing redirect_uri data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("code", authInfo.Code) @@ -130,21 +133,21 @@ func TestOAuthToken_AuthorizationCode(t *testing.T) { } func TestOAuthToken_ClientCredentials(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register a test client for client credentials - client := RegisterTestClient(t, "Client Credentials Test", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Client Credentials Test", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) t.Run("Valid Client Credentials Grant", func(t *testing.T) { - // Prepare token request + // testutils.Prepare token request data := url.Values{} data.Set("grant_type", "client_credentials") data.Set("scope", "api:read api:write") @@ -191,7 +194,7 @@ func TestOAuthToken_ClientCredentials(t *testing.T) { }) t.Run("Client Credentials Without Authentication", func(t *testing.T) { - // Prepare token request + // testutils.Prepare token request data := url.Values{} data.Set("grant_type", "client_credentials") @@ -213,21 +216,21 @@ func TestOAuthToken_ClientCredentials(t *testing.T) { } func TestOAuthToken_RefreshToken(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register a test client - client := RegisterTestClient(t, "Refresh Token Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Refresh Token Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) // First, get an access token and refresh token using authorization code - authInfo := ObtainAuthorizationCode(t, serverURL, client.ClientID, "https://localhost/callback", "openid profile") + authInfo := testutils.ObtainAuthorizationCode(t, serverURL, client.ClientID, "https://localhost/callback", "openid profile") // Get initial token with PKCE code verifier data := url.Values{} @@ -256,7 +259,7 @@ func TestOAuthToken_RefreshToken(t *testing.T) { assert.NotEmpty(t, initialToken.RefreshToken) t.Run("Valid Refresh Token Grant", func(t *testing.T) { - // Prepare refresh token request + // testutils.Prepare refresh token request data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("refresh_token", initialToken.RefreshToken) @@ -308,7 +311,7 @@ func TestOAuthToken_RefreshToken(t *testing.T) { }) t.Run("Invalid Refresh Token", func(t *testing.T) { - // Prepare refresh token request with invalid token + // testutils.Prepare refresh token request with invalid token data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("refresh_token", "invalid-refresh-token") @@ -329,7 +332,7 @@ func TestOAuthToken_RefreshToken(t *testing.T) { }) t.Run("Missing Refresh Token", func(t *testing.T) { - // Prepare refresh token request without refresh_token parameter + // testutils.Prepare refresh token request without refresh_token parameter data := url.Values{} data.Set("grant_type", "refresh_token") // Missing refresh_token @@ -351,20 +354,20 @@ func TestOAuthToken_RefreshToken(t *testing.T) { } func TestOAuthToken_InvalidGrantType(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } - client := RegisterTestClient(t, "Invalid Grant Test", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Invalid Grant Test", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) t.Run("Unsupported Grant Type", func(t *testing.T) { - // Prepare token request with unsupported grant type + // testutils.Prepare token request with unsupported grant type data := url.Values{} data.Set("grant_type", "unsupported_grant_type") @@ -389,7 +392,7 @@ func TestOAuthToken_InvalidGrantType(t *testing.T) { }) t.Run("Missing Grant Type", func(t *testing.T) { - // Prepare token request without grant_type + // testutils.Prepare token request without grant_type data := url.Values{} // Missing grant_type @@ -462,24 +465,24 @@ func base64Encode(data []byte) string { } func TestOAuthRevoke(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register a test client - client := RegisterTestClient(t, "Revoke Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Revoke Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) // Obtain access token directly using the utility function - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") t.Run("Valid Access Token Revocation", func(t *testing.T) { - // Prepare revocation request + // testutils.Prepare revocation request data := url.Values{} data.Set("token", tokenInfo.AccessToken) data.Set("token_type_hint", "access_token") @@ -503,7 +506,7 @@ func TestOAuthRevoke(t *testing.T) { }) t.Run("Valid Refresh Token Revocation", func(t *testing.T) { - // Prepare revocation request for refresh token + // testutils.Prepare revocation request for refresh token data := url.Values{} data.Set("token", tokenInfo.RefreshToken) data.Set("token_type_hint", "refresh_token") @@ -527,7 +530,7 @@ func TestOAuthRevoke(t *testing.T) { }) t.Run("Invalid Token Revocation", func(t *testing.T) { - // Prepare revocation request with invalid token + // testutils.Prepare revocation request with invalid token data := url.Values{} data.Set("token", "invalid-token-12345") data.Set("token_type_hint", "access_token") @@ -551,7 +554,7 @@ func TestOAuthRevoke(t *testing.T) { }) t.Run("Missing Token Parameter", func(t *testing.T) { - // Prepare revocation request without token parameter + // testutils.Prepare revocation request without token parameter data := url.Values{} // Missing token parameter @@ -573,24 +576,24 @@ func TestOAuthRevoke(t *testing.T) { } func TestOAuthIntrospect(t *testing.T) { - serverURL := Prepare(t) - defer Clean() + serverURL := testutils.Prepare(t) + defer testutils.Clean() // Get base URL from server config baseURL := "" - if Server != nil && Server.Config != nil { - baseURL = Server.Config.BaseURL + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL } // Register a test client - client := RegisterTestClient(t, "Introspect Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) + client := testutils.RegisterTestClient(t, "Introspect Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) // Obtain access token directly using the utility function - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") t.Run("Valid Access Token Introspection", func(t *testing.T) { - // Prepare introspection request + // testutils.Prepare introspection request data := url.Values{} data.Set("token", tokenInfo.AccessToken) data.Set("token_type_hint", "access_token") @@ -615,7 +618,7 @@ func TestOAuthIntrospect(t *testing.T) { assert.NoError(t, err) // Parse response directly (no wrapper) - var introspectResp TokenIntrospectionResponse + var introspectResp response.TokenIntrospectionResponse err = json.Unmarshal(body, &introspectResp) assert.NoError(t, err) @@ -630,7 +633,7 @@ func TestOAuthIntrospect(t *testing.T) { }) t.Run("Invalid Token Introspection", func(t *testing.T) { - // Prepare introspection request with invalid token + // testutils.Prepare introspection request with invalid token data := url.Values{} data.Set("token", "invalid-token-12345") data.Set("token_type_hint", "access_token") @@ -655,7 +658,7 @@ func TestOAuthIntrospect(t *testing.T) { assert.NoError(t, err) // Parse response directly (no wrapper) - var introspectResp TokenIntrospectionResponse + var introspectResp response.TokenIntrospectionResponse err = json.Unmarshal(body, &introspectResp) assert.NoError(t, err) @@ -666,7 +669,7 @@ func TestOAuthIntrospect(t *testing.T) { }) t.Run("Missing Token Parameter", func(t *testing.T) { - // Prepare introspection request without token parameter + // testutils.Prepare introspection request without token parameter data := url.Values{} // Missing token parameter @@ -727,7 +730,7 @@ func TestOAuthIntrospect(t *testing.T) { assert.NoError(t, err) // Parse response directly (no wrapper) - var introspectResp TokenIntrospectionResponse + var introspectResp response.TokenIntrospectionResponse err = json.Unmarshal(body, &introspectResp) assert.NoError(t, err) diff --git a/openapi/tests/openapi_test.go b/openapi/tests/openapi_test.go new file mode 100644 index 00000000..6769426c --- /dev/null +++ b/openapi/tests/openapi_test.go @@ -0,0 +1,41 @@ +package openapi_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/openapi" + "github.com/yaoapp/yao/openapi/tests/testutils" +) + +func TestLoad(t *testing.T) { + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + assert.NotNil(t, openapi.Server) + assert.NotEmpty(t, serverURL) + assert.Contains(t, serverURL, "http://127.0.0.1:") +} + +func TestObtainAccessToken(t *testing.T) { + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + // Register a test client + client := testutils.RegisterTestClient(t, "Token Utility Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + + // Test the ObtainAccessToken utility function + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile email") + + // Verify token information + assert.NotEmpty(t, tokenInfo.AccessToken, "Access token should not be empty") + assert.NotEmpty(t, tokenInfo.RefreshToken, "Refresh token should not be empty") + assert.Equal(t, "Bearer", tokenInfo.TokenType, "Token type should be Bearer") + assert.Greater(t, tokenInfo.ExpiresIn, 0, "ExpiresIn should be greater than 0") + assert.Equal(t, client.ClientID, tokenInfo.ClientID, "Client ID should match") + // Note: Scope might be empty in token response, which is valid + + t.Logf("Successfully obtained token: AccessToken=%s, TokenType=%s, ExpiresIn=%d, Scope=%s", + tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn, tokenInfo.Scope) +} diff --git a/openapi/openapi_test.go b/openapi/tests/testutils/testutils.go similarity index 88% rename from openapi/openapi_test.go rename to openapi/tests/testutils/testutils.go index 897ac320..c99d2aab 100644 --- a/openapi/openapi_test.go +++ b/openapi/tests/testutils/testutils.go @@ -1,4 +1,4 @@ -package openapi +package testutils import ( "context" @@ -8,12 +8,13 @@ import ( "fmt" "net" "net/http" + "sync" "testing" "time" "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/openapi" "github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/test" ) @@ -21,6 +22,12 @@ import ( // testServer holds the test HTTP server instance var testServer *http.Server +// testMutex protects global state access during concurrent test execution +var testMutex sync.RWMutex + +// activeTestCount tracks the number of active tests using the global server +var activeTestCount int + // Prepare initializes the OpenAPI test environment and starts a mock HTTP server. // // AI ASSISTANT INSTRUCTIONS: @@ -85,13 +92,22 @@ var testServer *http.Server // ERROR HANDLING: // If any step fails, the test will fail immediately with a descriptive error message. func Prepare(t *testing.T) string { + // Use write lock to protect global state initialization + testMutex.Lock() + defer func() { + activeTestCount++ + testMutex.Unlock() + }() + // Step 1: Initialize base test environment with all Yao dependencies test.Prepare(t, config.Conf) - // Step 2: Initialize OpenAPI server and make it available globally - _, err := Load(config.Conf) - if err != nil { - t.Fatalf("Failed to load OpenAPI server: %v", err) + // Step 2: Initialize OpenAPI server and make it available globally (only if not already initialized) + if openapi.Server == nil { + _, err := openapi.Load(config.Conf) + if err != nil { + t.Fatalf("Failed to load OpenAPI server: %v", err) + } } // Step 3: Create Gin router and attach OpenAPI server @@ -99,8 +115,8 @@ func Prepare(t *testing.T) string { router := gin.New() // Attach the OpenAPI server to the router - if Server != nil { - Server.Attach(router) + if openapi.Server != nil { + openapi.Server.Attach(router) } // Step 4: Start HTTP server on random available port @@ -109,13 +125,13 @@ func Prepare(t *testing.T) string { t.Fatalf("Failed to create listener: %v", err) } - testServer = &http.Server{ + server := &http.Server{ Handler: router, } // Start server in background go func() { - if err := testServer.Serve(listener); err != nil && err != http.ErrServerClosed { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { t.Errorf("Failed to start test server: %v", err) } }() @@ -123,6 +139,9 @@ func Prepare(t *testing.T) string { // Wait a moment for server to start time.Sleep(10 * time.Millisecond) + // Store server instance for this test (each test gets its own HTTP server) + testServer = server + // Return server URL serverURL := fmt.Sprintf("http://%s", listener.Addr().String()) return serverURL @@ -154,7 +173,7 @@ func Prepare(t *testing.T) string { // - The order of cleanup steps is important: HTTP server first, then OpenAPI cleanup, then base cleanup // - Server shutdown has a 5-second timeout to prevent hanging tests func Clean() { - // Step 1: Gracefully shutdown the HTTP test server + // Step 1: Gracefully shutdown the HTTP test server for this test if testServer != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -166,11 +185,21 @@ func Clean() { testServer = nil } - // Step 2: Reset OpenAPI server instance to prevent state leakage - Server = nil + // Step 2: Use lock to safely decrement active test count and clean global state if needed + testMutex.Lock() + activeTestCount-- + shouldCleanGlobalState := activeTestCount <= 0 + if shouldCleanGlobalState { + // Reset global state only when no other tests are active + openapi.Server = nil + activeTestCount = 0 // Ensure it doesn't go negative + } + testMutex.Unlock() - // Step 3: Clean up base test environment and all dependencies - test.Clean() + // Step 3: Clean up base test environment + if shouldCleanGlobalState { + test.Clean() + } } // RegisterTestClient registers a test OAuth client and returns the client information. @@ -209,7 +238,11 @@ func Clean() { // ERROR HANDLING: // If client registration fails, the test will fail immediately with a descriptive error message. func RegisterTestClient(t *testing.T, clientName string, redirectURIs []string) *types.ClientInfo { - if Server == nil || Server.OAuth == nil { + testMutex.RLock() + server := openapi.Server + testMutex.RUnlock() + + if server == nil || server.OAuth == nil { t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.") } @@ -232,7 +265,7 @@ func RegisterTestClient(t *testing.T, clientName string, redirectURIs []string) // Register the client using the OAuth service ctx := context.Background() - response, err := Server.OAuth.DynamicClientRegistration(ctx, req) + response, err := server.OAuth.DynamicClientRegistration(ctx, req) if err != nil { t.Fatalf("Failed to register test client: %v", err) } @@ -279,7 +312,11 @@ func RegisterTestClient(t *testing.T, clientName string, redirectURIs []string) // If client deletion fails, logs an error but does not fail the test. // This prevents cleanup failures from affecting test results. func CleanupTestClient(t *testing.T, clientID string) { - if Server == nil || Server.OAuth == nil { + testMutex.RLock() + server := openapi.Server + testMutex.RUnlock() + + if server == nil || server.OAuth == nil { // Server might already be cleaned up, which is OK return } @@ -290,7 +327,7 @@ func CleanupTestClient(t *testing.T, clientID string) { // Delete the client using the OAuth service ctx := context.Background() - err := Server.OAuth.DeleteClient(ctx, clientID) + err := server.OAuth.DeleteClient(ctx, clientID) if err != nil { // Log error but don't fail the test - cleanup should be resilient t.Logf("Warning: Failed to cleanup test client %s: %v", clientID, err) @@ -321,6 +358,7 @@ func CreateTestClientCredentials() (clientID, clientSecret string) { return "test-client-id", "test-client-secret" } +// AuthorizationInfo represents the information needed for OAuth authorization. // ObtainAuthorizationCode dynamically obtains an authorization code for testing OAuth token endpoints. // // AI ASSISTANT INSTRUCTIONS: @@ -383,8 +421,13 @@ type AuthorizationInfo struct { CodeChallengeMethod string } +// ObtainAuthorizationCode obtains an authorization code for testing OAuth token endpoints. func ObtainAuthorizationCode(t *testing.T, serverURL, clientID, redirectURI, scope string) *AuthorizationInfo { - if Server == nil || Server.OAuth == nil { + testMutex.RLock() + server := openapi.Server + testMutex.RUnlock() + + if server == nil || server.OAuth == nil { t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.") } @@ -409,7 +452,7 @@ func ObtainAuthorizationCode(t *testing.T, serverURL, clientID, redirectURI, sco // Call OAuth service to process authorization request ctx := context.Background() - authResp, err := Server.OAuth.Authorize(ctx, authReq) + authResp, err := server.OAuth.Authorize(ctx, authReq) if err != nil { t.Fatalf("Failed to obtain authorization code: %v", err) } @@ -439,6 +482,7 @@ func ObtainAuthorizationCode(t *testing.T, serverURL, clientID, redirectURI, sco return authInfo } +// TokenInfo represents the information needed for OAuth token exchange. // ObtainAccessToken directly obtains an access token for testing OAuth endpoints that require authentication. // // AI ASSISTANT INSTRUCTIONS: @@ -495,8 +539,13 @@ type TokenInfo struct { ClientID string } +// ObtainAccessToken obtains an access token for testing OAuth endpoints that require authentication. func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirectURI, scope string) *TokenInfo { - if Server == nil || Server.OAuth == nil { + testMutex.RLock() + server := openapi.Server + testMutex.RUnlock() + + if server == nil || server.OAuth == nil { t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.") } @@ -505,7 +554,7 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect // Step 2: Exchange authorization code for access token with PKCE code verifier ctx := context.Background() - token, err := Server.OAuth.Token(ctx, "authorization_code", authInfo.Code, clientID, authInfo.CodeVerifier) + token, err := server.OAuth.Token(ctx, "authorization_code", authInfo.Code, clientID, authInfo.CodeVerifier) if err != nil { t.Fatalf("Failed to exchange authorization code for token: %v", err) } @@ -529,38 +578,6 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect return tokenInfo } -func TestLoad(t *testing.T) { - serverURL := Prepare(t) - defer Clean() - - assert.NotNil(t, Server) - assert.NotEmpty(t, serverURL) - assert.Contains(t, serverURL, "http://127.0.0.1:") -} - -func TestObtainAccessToken(t *testing.T) { - serverURL := Prepare(t) - defer Clean() - - // Register a test client - client := RegisterTestClient(t, "Token Utility Test Client", []string{"https://localhost/callback"}) - defer CleanupTestClient(t, client.ClientID) - - // Test the ObtainAccessToken utility function - tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile email") - - // Verify token information - assert.NotEmpty(t, tokenInfo.AccessToken, "Access token should not be empty") - assert.NotEmpty(t, tokenInfo.RefreshToken, "Refresh token should not be empty") - assert.Equal(t, "Bearer", tokenInfo.TokenType, "Token type should be Bearer") - assert.Greater(t, tokenInfo.ExpiresIn, 0, "ExpiresIn should be greater than 0") - assert.Equal(t, client.ClientID, tokenInfo.ClientID, "Client ID should match") - // Note: Scope might be empty in token response, which is valid - - t.Logf("Successfully obtained token: AccessToken=%s, TokenType=%s, ExpiresIn=%d, Scope=%s", - tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn, tokenInfo.Scope) -} - // generateCodeVerifier generates a cryptographically random code verifier for PKCE func generateCodeVerifier() string { // PKCE code verifier should be 43-128 characters long