Merge pull request #1037 from trheyi/main

Remove deprecated test files and refactor OAuth response handling
This commit is contained in:
Max 2025-07-24 15:53:55 +08:00 committed by GitHub
commit 6d6230fe5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 524 additions and 338 deletions

View file

@ -1,9 +1,11 @@
package kb package kb
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yaoapp/gou/graphrag/types"
"github.com/yaoapp/yao/kb" "github.com/yaoapp/yao/kb"
) )
@ -11,8 +13,44 @@ import (
// CreateCollection creates a new collection // CreateCollection creates a new collection
func CreateCollection(c *gin.Context) { func CreateCollection(c *gin.Context) {
// TODO: Implement create collection logic var req CreateCollectionRequest
c.JSON(http.StatusCreated, gin.H{"message": "Collection created"})
// 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 // RemoveCollection removes an existing collection
@ -36,3 +74,28 @@ func GetCollections(c *gin.Context) {
} }
c.JSON(http.StatusOK, collections) 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
}

View file

@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yaoapp/yao/openapi/oauth" "github.com/yaoapp/yao/openapi/oauth"
"github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
) )
// OAuth handlers // OAuth handlers
@ -73,7 +74,7 @@ func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) {
// Parse and validate authorization request // Parse and validate authorization request
authReq, parseErr := openapi.parseAuthorizationRequest(c) authReq, parseErr := openapi.parseAuthorizationRequest(c)
if parseErr != nil { if parseErr != nil {
openapi.respondWithAuthorizationError(c, authReq.RedirectURI, parseErr, authReq.State) response.RespondWithAuthorizationError(c, authReq.RedirectURI, parseErr, authReq.State)
return return
} }
@ -81,18 +82,18 @@ func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) {
authResp, err := openapi.OAuth.Authorize(c, authReq) authResp, err := openapi.OAuth.Authorize(c, authReq)
if err != nil { if err != nil {
// OAuth service returned an error // OAuth service returned an error
openapi.respondWithAuthorizationError(c, authReq.RedirectURI, ErrServerError, authReq.State) response.RespondWithAuthorizationError(c, authReq.RedirectURI, response.ErrServerError, authReq.State)
return return
} }
// Check if authorization response contains an error // Check if authorization response contains an error
if authResp.Error != "" { if authResp.Error != "" {
// Convert OAuth service error to ErrorResponse // Convert OAuth service error to ErrorResponse
oauthError := &ErrorResponse{ oauthError := &response.ErrorResponse{
Code: authResp.Error, Code: authResp.Error,
ErrorDescription: authResp.ErrorDescription, ErrorDescription: authResp.ErrorDescription,
} }
openapi.respondWithAuthorizationError(c, authReq.RedirectURI, oauthError, authReq.State) response.RespondWithAuthorizationError(c, authReq.RedirectURI, oauthError, authReq.State)
return 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) // 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 // oauthToken handles token requests - RFC 6749 Section 3.2
@ -123,7 +124,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) {
// Validate grant type // Validate grant type
if grantType == "" { if grantType == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
@ -141,7 +142,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) {
openapi.handleTokenExchangeGrant(c) openapi.handleTokenExchangeGrant(c)
default: 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 // Extract client credentials from Basic Auth header or form parameters
clientID, clientSecret := openapi.extractClientCredentials(c) clientID, clientSecret := openapi.extractClientCredentials(c)
if clientID == "" { if clientID == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient)
return return
} }
// Validate client credentials using OAuth service // Validate client credentials using OAuth service
oauthService, ok := openapi.OAuth.(*oauth.Service) oauthService, ok := openapi.OAuth.(*oauth.Service)
if !ok { if !ok {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient)
return return
} }
clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret)
if err != nil { if err != nil {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient)
return return
} }
@ -179,13 +180,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Basic validation for authorization code grant // Basic validation for authorization code grant
if code == "" || redirectURI == "" { if code == "" || redirectURI == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// Validate that client supports authorization code grant // Validate that client supports authorization code grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeAuthorizationCode) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeAuthorizationCode) {
openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient)
return return
} }
@ -194,13 +195,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Basic validation for device code grant // Basic validation for device code grant
if code == "" { if code == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// Validate that client supports device code grant // Validate that client supports device code grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeDeviceCode) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeDeviceCode) {
openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient)
return return
} }
@ -210,7 +211,7 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Validate that client supports client credentials grant // Validate that client supports client credentials grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeClientCredentials) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeClientCredentials) {
openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient)
return return
} }
} }
@ -219,16 +220,16 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
token, err := openapi.OAuth.Token(c, grantType, code, clientID, codeVerifier) token, err := openapi.OAuth.Token(c, grantType, code, clientID, codeVerifier)
if err != nil { if err != nil {
// Convert OAuth service error to token error response with security headers // Convert OAuth service error to token error response with security headers
if oauthErr, ok := err.(*ErrorResponse); ok { if oauthErr, ok := err.(*response.ErrorResponse); ok {
openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) response.RespondWithSecureError(c, response.StatusBadRequest, oauthErr)
} else { } else {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidGrant)
} }
return return
} }
// Return successful token response with OAuth security headers (RFC 6749 Section 5.1: MUST set Cache-Control: no-store) // 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 // 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 // Extract client credentials from Basic Auth header or form parameters
clientID, clientSecret := openapi.extractClientCredentials(c) clientID, clientSecret := openapi.extractClientCredentials(c)
if clientID == "" { if clientID == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient)
return return
} }
// Validate client credentials using OAuth service // Validate client credentials using OAuth service
oauthService, ok := openapi.OAuth.(*oauth.Service) oauthService, ok := openapi.OAuth.(*oauth.Service)
if !ok { if !ok {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient)
return return
} }
clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret)
if err != nil { if err != nil {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClient)
return return
} }
// Validate that client supports refresh token grant // Validate that client supports refresh token grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeRefreshToken) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeRefreshToken) {
openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) response.RespondWithSecureError(c, response.StatusUnauthorized, response.ErrUnauthorizedClient)
return return
} }
@ -264,7 +265,7 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) {
// Basic validation // Basic validation
if refreshToken == "" { if refreshToken == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
@ -277,16 +278,16 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) {
} }
if err != nil { if err != nil {
// Convert OAuth service error to token error response with security headers // Convert OAuth service error to token error response with security headers
if oauthErr, ok := err.(*ErrorResponse); ok { if oauthErr, ok := err.(*response.ErrorResponse); ok {
openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) response.RespondWithSecureError(c, response.StatusBadRequest, oauthErr)
} else { } else {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidGrant)
} }
return return
} }
// Return successful refresh token response with security headers // 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 // handleTokenExchangeGrant handles token exchange requests - RFC 8693
@ -298,7 +299,7 @@ func (openapi *OpenAPI) handleTokenExchangeGrant(c *gin.Context) {
// Basic validation // Basic validation
if subjectToken == "" { if subjectToken == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
@ -306,16 +307,16 @@ func (openapi *OpenAPI) handleTokenExchangeGrant(c *gin.Context) {
exchangeResponse, err := openapi.OAuth.TokenExchange(c, subjectToken, subjectTokenType, audience, scope) exchangeResponse, err := openapi.OAuth.TokenExchange(c, subjectToken, subjectTokenType, audience, scope)
if err != nil { if err != nil {
// Convert OAuth service error to token error response with security headers // Convert OAuth service error to token error response with security headers
if oauthErr, ok := err.(*ErrorResponse); ok { if oauthErr, ok := err.(*response.ErrorResponse); ok {
openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) response.RespondWithSecureError(c, response.StatusBadRequest, oauthErr)
} else { } else {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidGrant)
} }
return return
} }
// Return successful token exchange response with security headers // 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 // 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 tokenTypeHint := c.PostForm("token_type_hint") // Optional hint about token type
if token == "" { if token == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
@ -333,14 +334,14 @@ func (openapi *OpenAPI) oauthRevoke(c *gin.Context) {
if err != nil { if err != nil {
// OAuth spec requires returning 200 even for invalid tokens to prevent information leakage // OAuth spec requires returning 200 even for invalid tokens to prevent information leakage
// Only return error for server errors // Only return error for server errors
if oauthErr, ok := err.(*ErrorResponse); ok && oauthErr.Code == ErrServerError.Code { if oauthErr, ok := err.(*response.ErrorResponse); ok && oauthErr.Code == response.ErrServerError.Code {
openapi.respondWithError(c, StatusInternalServerError, ErrServerError) response.RespondWithError(c, response.StatusInternalServerError, response.ErrServerError)
return return
} }
} }
// RFC 7009: Return 200 OK for successful revocation (or invalid tokens) // RFC 7009: Return 200 OK for successful revocation (or invalid tokens)
c.Status(StatusOK) c.Status(response.StatusOK)
} }
// oauthIntrospect handles token introspection - RFC 7662 // oauthIntrospect handles token introspection - RFC 7662
@ -348,7 +349,7 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {
token := c.PostForm("token") token := c.PostForm("token")
if token == "" { if token == "" {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
@ -356,15 +357,15 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {
introspectionResult, err := openapi.OAuth.Introspect(c, token) introspectionResult, err := openapi.OAuth.Introspect(c, token)
if err != nil { if err != nil {
// Return inactive token response on error (RFC 7662) with security headers // Return inactive token response on error (RFC 7662) with security headers
response := &TokenIntrospectionResponse{ tokenResponse := &response.TokenIntrospectionResponse{
Active: false, Active: false,
} }
openapi.respondWithSecureSuccess(c, StatusOK, response) response.RespondWithSecureSuccess(c, response.StatusOK, tokenResponse)
return return
} }
// Convert OAuth service response to API response format // Convert OAuth service response to API response format
response := &TokenIntrospectionResponse{ tokenResponse := &response.TokenIntrospectionResponse{
Active: introspectionResult.Active, Active: introspectionResult.Active,
Scope: introspectionResult.Scope, Scope: introspectionResult.Scope,
ClientID: introspectionResult.ClientID, ClientID: introspectionResult.ClientID,
@ -376,19 +377,19 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {
Audience: introspectionResult.Audience, Audience: introspectionResult.Audience,
} }
openapi.respondWithSecureSuccess(c, StatusOK, response) response.RespondWithSecureSuccess(c, response.StatusOK, tokenResponse)
} }
// oauthJWKS returns JSON Web Key Set - RFC 7517 // oauthJWKS returns JSON Web Key Set - RFC 7517
func (openapi *OpenAPI) oauthJWKS(c *gin.Context) { func (openapi *OpenAPI) oauthJWKS(c *gin.Context) {
jwks, err := openapi.OAuth.JWKS(c) jwks, err := openapi.OAuth.JWKS(c)
if err != nil { if err != nil {
openapi.respondWithError(c, StatusInternalServerError, ErrServerError) response.RespondWithError(c, response.StatusInternalServerError, response.ErrServerError)
return return
} }
// RFC 7517 compliance: Return JWKS directly with security headers // 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 // 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 // Check for Bearer token in Authorization header
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " { if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " {
openapi.respondWithError(c, StatusUnauthorized, ErrInvalidToken) response.RespondWithError(c, response.StatusUnauthorized, response.ErrInvalidToken)
return return
} }
// TODO: Implement user info retrieval // TODO: Implement user info retrieval
openapi.respondWithError(c, StatusNotImplemented, ErrServerError) response.RespondWithError(c, response.StatusNotImplemented, response.ErrServerError)
} }
// OAuth Extended Endpoints Implementation // OAuth Extended Endpoints Implementation
// oauthRegister handles dynamic client registration - RFC 7591 // oauthRegister handles dynamic client registration - RFC 7591
func (openapi *OpenAPI) oauthRegister(c *gin.Context) { func (openapi *OpenAPI) oauthRegister(c *gin.Context) {
var req DynamicClientRegistrationRequest var req response.DynamicClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClientMetadata)
return return
} }
// Basic validation // Basic validation
if len(req.RedirectURIs) == 0 { if len(req.RedirectURIs) == 0 {
openapi.respondWithSecureError(c, StatusBadRequest, ErrMissingRedirectURI) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrMissingRedirectURI)
return return
} }
res, err := openapi.OAuth.DynamicClientRegistration(c, &req) res, err := openapi.OAuth.DynamicClientRegistration(c, &req)
if err != nil { if err != nil {
openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata) response.RespondWithSecureError(c, response.StatusBadRequest, response.ErrInvalidClientMetadata)
return return
} }
// Return the authorization response with security headers (RFC 7591 compliant, contains client credentials) // 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 // 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") clientID := c.Param("client_id")
if clientID == "" { if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// TODO: Implement client retrieval logic // TODO: Implement client retrieval logic
openapi.respondWithError(c, StatusNotFound, ErrInvalidClient) response.RespondWithError(c, response.StatusNotFound, response.ErrInvalidClient)
} }
// oauthUpdateClient updates client configuration - RFC 7592 // oauthUpdateClient updates client configuration - RFC 7592
@ -492,18 +493,18 @@ func (openapi *OpenAPI) oauthUpdateClient(c *gin.Context) {
clientID := c.Param("client_id") clientID := c.Param("client_id")
if clientID == "" { if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
var req DynamicClientRegistrationRequest var req response.DynamicClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidClientMetadata)
return return
} }
// TODO: Implement client update logic // TODO: Implement client update logic
openapi.respondWithError(c, StatusNotImplemented, ErrServerError) response.RespondWithError(c, response.StatusNotImplemented, response.ErrServerError)
} }
// oauthDeleteClient deletes client configuration - RFC 7592 // oauthDeleteClient deletes client configuration - RFC 7592
@ -511,12 +512,12 @@ func (openapi *OpenAPI) oauthDeleteClient(c *gin.Context) {
clientID := c.Param("client_id") clientID := c.Param("client_id")
if clientID == "" { if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// TODO: Implement client deletion logic // TODO: Implement client deletion logic
c.Status(StatusNoContent) c.Status(response.StatusNoContent)
} }
// oauthDeviceAuthorization handles device authorization - RFC 8628 // oauthDeviceAuthorization handles device authorization - RFC 8628
@ -524,12 +525,12 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) {
clientID := c.PostForm("client_id") clientID := c.PostForm("client_id")
if clientID == "" { if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// TODO: Implement device authorization logic // TODO: Implement device authorization logic
response := &DeviceAuthorizationResponse{ deviceResponse := &response.DeviceAuthorizationResponse{
DeviceCode: "generated-device-code", DeviceCode: "generated-device-code",
UserCode: "USER-CODE", UserCode: "USER-CODE",
VerificationURI: "https://example.com/device", VerificationURI: "https://example.com/device",
@ -537,31 +538,31 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) {
Interval: 5, // 5 seconds Interval: 5, // 5 seconds
} }
openapi.respondWithSuccess(c, StatusOK, response) response.RespondWithSuccess(c, response.StatusOK, deviceResponse)
} }
// oauthPushedAuthorizationRequest handles PAR - RFC 9126 // oauthPushedAuthorizationRequest handles PAR - RFC 9126
func (openapi *OpenAPI) oauthPushedAuthorizationRequest(c *gin.Context) { func (openapi *OpenAPI) oauthPushedAuthorizationRequest(c *gin.Context) {
var req PushedAuthorizationRequest var req response.PushedAuthorizationRequest
if err := c.ShouldBind(&req); err != nil { if err := c.ShouldBind(&req); err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// Basic validation // Basic validation
if req.ClientID == "" { if req.ClientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// TODO: Implement PAR logic // TODO: Implement PAR logic
response := &PushedAuthorizationResponse{ parResponse := &response.PushedAuthorizationResponse{
RequestURI: "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2", RequestURI: "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2",
ExpiresIn: 60, // 60 seconds ExpiresIn: 60, // 60 seconds
} }
openapi.respondWithSuccess(c, StatusCreated, response) response.RespondWithSuccess(c, response.StatusCreated, parResponse)
} }
// oauthTokenExchange handles token exchange - RFC 8693 // oauthTokenExchange handles token exchange - RFC 8693
@ -569,29 +570,29 @@ func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) {
grantType := c.PostForm("grant_type") grantType := c.PostForm("grant_type")
if grantType != types.GrantTypeTokenExchange { if grantType != types.GrantTypeTokenExchange {
openapi.respondWithError(c, StatusBadRequest, ErrUnsupportedGrantType) response.RespondWithError(c, response.StatusBadRequest, response.ErrUnsupportedGrantType)
return return
} }
subjectToken := c.PostForm("subject_token") subjectToken := c.PostForm("subject_token")
if subjectToken == "" { if subjectToken == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) response.RespondWithError(c, response.StatusBadRequest, response.ErrInvalidRequest)
return return
} }
// TODO: Implement token exchange logic // TODO: Implement token exchange logic
response := &TokenExchangeResponse{ exchangeResponse := &response.TokenExchangeResponse{
AccessToken: "exchanged-access-token", AccessToken: "exchanged-access-token",
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: types.TokenTypeBearer, TokenType: types.TokenTypeBearer,
ExpiresIn: 3600, // 1 hour ExpiresIn: 3600, // 1 hour
} }
openapi.respondWithSuccess(c, StatusOK, response) response.RespondWithSuccess(c, response.StatusOK, exchangeResponse)
} }
// parseAuthorizationRequest parses and validates authorization request parameters // 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 // Parse authorization request parameters from both GET (query) and POST (form) methods
authReq := &types.AuthorizationRequest{ authReq := &types.AuthorizationRequest{
ClientID: openapi.getParam(c, "client_id"), ClientID: openapi.getParam(c, "client_id"),
@ -607,12 +608,12 @@ func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*types.Author
// Basic validation // Basic validation
if authReq.ClientID == "" { if authReq.ClientID == "" {
return authReq, ErrInvalidRequest return authReq, response.ErrInvalidRequest
} }
// Validate response_type parameter - RFC 6749 Section 3.1.1 // Validate response_type parameter - RFC 6749 Section 3.1.1
if authReq.ResponseType == "" { if authReq.ResponseType == "" {
return authReq, ErrInvalidRequest return authReq, response.ErrInvalidRequest
} }
// Check supported response types // Check supported response types
@ -621,9 +622,9 @@ func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*types.Author
// Authorization code flow - supported // Authorization code flow - supported
case types.ResponseTypeToken: case types.ResponseTypeToken:
// Implicit flow - deprecated in OAuth 2.1, return error // Implicit flow - deprecated in OAuth 2.1, return error
return authReq, ErrUnsupportedResponseType return authReq, response.ErrUnsupportedResponseType
default: default:
return authReq, ErrUnsupportedResponseType return authReq, response.ErrUnsupportedResponseType
} }
return authReq, nil return authReq, nil

View file

@ -1,4 +1,4 @@
package openapi package response
import ( import (
"net/http" "net/http"
@ -147,7 +147,7 @@ const (
// setOAuthSecurityHeaders sets standard OAuth 2.0/2.1 security headers // setOAuthSecurityHeaders sets standard OAuth 2.0/2.1 security headers
// These headers are required by OAuth 2.1 specification for enhanced security // 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("Cache-Control", "no-store")
c.Header("Pragma", "no-cache") c.Header("Pragma", "no-cache")
c.Header("X-Content-Type-Options", "nosniff") 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 // 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") c.Header("Content-Type", "application/json")
} }
// respondWithSuccess sends a successful response (no wrapper, direct data) // RespondWithSuccess sends a successful response (no wrapper, direct data)
func (openapi *OpenAPI) respondWithSuccess(c *gin.Context, statusCode int, data interface{}) { func RespondWithSuccess(c *gin.Context, statusCode int, data interface{}) {
openapi.setJSONContentType(c) SetJSONContentType(c)
c.JSON(statusCode, data) c.JSON(statusCode, data)
} }
// respondWithError sends an error response (no wrapper, direct error) // RespondWithError sends an error response (no wrapper, direct error)
func (openapi *OpenAPI) respondWithError(c *gin.Context, statusCode int, err *ErrorResponse) { func RespondWithError(c *gin.Context, statusCode int, err *ErrorResponse) {
openapi.setJSONContentType(c) SetJSONContentType(c)
// Add WWW-Authenticate header for 401 responses // Add WWW-Authenticate header for 401 responses
if statusCode == StatusUnauthorized { if statusCode == StatusUnauthorized {
openapi.addWWWAuthenticateHeader(c, err) AddWWWAuthenticateHeader(c, err)
} }
c.JSON(statusCode, err) c.JSON(statusCode, err)
} }
// respondWithAuthorizationError sends an authorization endpoint error via redirect // RespondWithAuthorizationError sends an authorization endpoint error via redirect
func (openapi *OpenAPI) respondWithAuthorizationError(c *gin.Context, redirectURI string, err *ErrorResponse, state string) { func RespondWithAuthorizationError(c *gin.Context, redirectURI string, err *ErrorResponse, state string) {
// Build error redirect URL // Build error redirect URL
redirectURL := redirectURI redirectURL := redirectURI
if redirectURL != "" { if redirectURL != "" {
@ -204,11 +204,11 @@ func (openapi *OpenAPI) respondWithAuthorizationError(c *gin.Context, redirectUR
} }
// Fallback to JSON error response if no redirect URI // Fallback to JSON error response if no redirect URI
openapi.respondWithError(c, StatusBadRequest, err) RespondWithError(c, StatusBadRequest, err)
} }
// addWWWAuthenticateHeader adds appropriate WWW-Authenticate header // AddWWWAuthenticateHeader adds appropriate WWW-Authenticate header
func (openapi *OpenAPI) addWWWAuthenticateHeader(c *gin.Context, err *ErrorResponse) { func AddWWWAuthenticateHeader(c *gin.Context, err *ErrorResponse) {
challenge := &WWWAuthenticateChallenge{ challenge := &WWWAuthenticateChallenge{
Scheme: types.WWWAuthenticateSchemeBearer, Scheme: types.WWWAuthenticateSchemeBearer,
Realm: "OAuth", Realm: "OAuth",
@ -238,21 +238,21 @@ func (openapi *OpenAPI) addWWWAuthenticateHeader(c *gin.Context, err *ErrorRespo
c.Header("WWW-Authenticate", headerValue) c.Header("WWW-Authenticate", headerValue)
} }
// respondWithSecureSuccess sends a successful response with OAuth security headers (for sensitive endpoints) // RespondWithSecureSuccess sends a successful response with OAuth security headers (for sensitive endpoints)
func (openapi *OpenAPI) respondWithSecureSuccess(c *gin.Context, statusCode int, data interface{}) { func RespondWithSecureSuccess(c *gin.Context, statusCode int, data interface{}) {
openapi.setOAuthSecurityHeaders(c) SetOAuthSecurityHeaders(c)
openapi.setJSONContentType(c) SetJSONContentType(c)
c.JSON(statusCode, data) c.JSON(statusCode, data)
} }
// respondWithSecureError sends an error response with OAuth security headers (for sensitive endpoints) // RespondWithSecureError sends an error response with OAuth security headers (for sensitive endpoints)
func (openapi *OpenAPI) respondWithSecureError(c *gin.Context, statusCode int, err *ErrorResponse) { func RespondWithSecureError(c *gin.Context, statusCode int, err *ErrorResponse) {
openapi.setOAuthSecurityHeaders(c) SetOAuthSecurityHeaders(c)
openapi.setJSONContentType(c) SetJSONContentType(c)
// Add WWW-Authenticate header for 401 responses // Add WWW-Authenticate header for 401 responses
if statusCode == StatusUnauthorized { if statusCode == StatusUnauthorized {
openapi.addWWWAuthenticateHeader(c, err) AddWWWAuthenticateHeader(c, err)
} }
c.JSON(statusCode, err) c.JSON(statusCode, err)

View file

@ -1,11 +1,14 @@
package openapi package openapi_test
import ( import (
"path/filepath"
"strings"
"testing" "testing"
"time" "time"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/oauth/types" "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) err := jsoniter.Unmarshal([]byte(jsonData), &config)
assert.NoError(t, err, "JSON unmarshaling should succeed") assert.NoError(t, err, "JSON unmarshaling should succeed")
@ -111,11 +114,11 @@ func TestFormatDuration(t *testing.T) {
func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) { func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) {
// Create a config with duration fields // Create a config with duration fields
originalConfig := &Config{ originalConfig := &openapi.Config{
BaseURL: "/v1", BaseURL: "/v1",
Store: "__yao.oauth.store", Store: "__yao.oauth.store",
Cache: "__yao.oauth.cache", Cache: "__yao.oauth.cache",
OAuth: &OAuth{ OAuth: &openapi.OAuth{
IssuerURL: "https://localhost:5099", IssuerURL: "https://localhost:5099",
Signing: types.SigningConfig{ Signing: types.SigningConfig{
SigningCertPath: "/path/to/cert.pem", SigningCertPath: "/path/to/cert.pem",
@ -145,7 +148,7 @@ func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) {
assert.NoError(t, err, "Marshal should succeed") assert.NoError(t, err, "Marshal should succeed")
// Unmarshal back to config // Unmarshal back to config
var unmarshaledConfig Config var unmarshaledConfig openapi.Config
err = jsoniter.Unmarshal(jsonData, &unmarshaledConfig) err = jsoniter.Unmarshal(jsonData, &unmarshaledConfig)
assert.NoError(t, err, "Unmarshal should succeed") assert.NoError(t, err, "Unmarshal should succeed")
@ -178,11 +181,11 @@ func TestConfigMarshalUnmarshalRoundTrip(t *testing.T) {
// TestConfigJSONOutputDemo demonstrates the human-readable JSON output format // TestConfigJSONOutputDemo demonstrates the human-readable JSON output format
func TestConfigJSONOutputDemo(t *testing.T) { func TestConfigJSONOutputDemo(t *testing.T) {
config := &Config{ config := &openapi.Config{
BaseURL: "/v1", BaseURL: "/v1",
Store: "__yao.oauth.store", Store: "__yao.oauth.store",
Cache: "__yao.oauth.cache", Cache: "__yao.oauth.cache",
OAuth: &OAuth{ OAuth: &openapi.OAuth{
IssuerURL: "https://localhost:5099", IssuerURL: "https://localhost:5099",
Signing: types.SigningConfig{ Signing: types.SigningConfig{
SigningCertPath: "openapi/certs/signing-cert.pem", 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
}

View file

@ -1,4 +1,4 @@
package openapi package openapi_test
import ( import (
"bytes" "bytes"
@ -10,23 +10,25 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/dsl/types" "github.com/yaoapp/yao/dsl/types"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/tests/testutils"
) )
// TestDSLCreate tests the DSL creation endpoint // TestDSLCreate tests the DSL creation endpoint
func TestDSLCreate(t *testing.T) { func TestDSLCreate(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register test client and get token // Register test client and get token
client := RegisterTestClient(t, "DSL Create Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Create Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
// Generate unique test ID // Generate unique test ID
testID := fmt.Sprintf("test_model_%d", time.Now().UnixNano()) testID := fmt.Sprintf("test_model_%d", time.Now().UnixNano())
@ -51,7 +53,7 @@ func TestDSLCreate(t *testing.T) {
for _, store := range stores { for _, store := range stores {
t.Run(fmt.Sprintf("CreateModel_%s", store), func(t *testing.T) { t.Run(fmt.Sprintf("CreateModel_%s", store), func(t *testing.T) {
// Prepare request body // testutils.Prepare request body
createData := map[string]interface{}{ createData := map[string]interface{}{
"id": testID + "_" + store, "id": testID + "_" + store,
"source": modelSource, "source": modelSource,
@ -89,17 +91,17 @@ func TestDSLCreate(t *testing.T) {
// TestDSLInspect tests the DSL inspection endpoint // TestDSLInspect tests the DSL inspection endpoint
func TestDSLInspect(t *testing.T) { func TestDSLInspect(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL Inspect Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Inspect Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
testID := fmt.Sprintf("test_inspect_%d", time.Now().UnixNano()) testID := fmt.Sprintf("test_inspect_%d", time.Now().UnixNano())
modelSource := fmt.Sprintf(`{ modelSource := fmt.Sprintf(`{
@ -162,17 +164,17 @@ func TestDSLInspect(t *testing.T) {
// TestDSLSource tests the DSL source retrieval endpoint // TestDSLSource tests the DSL source retrieval endpoint
func TestDSLSource(t *testing.T) { func TestDSLSource(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL Source Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Source Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
testID := fmt.Sprintf("test_source_%d", time.Now().UnixNano()) testID := fmt.Sprintf("test_source_%d", time.Now().UnixNano())
modelSource := fmt.Sprintf(`{ modelSource := fmt.Sprintf(`{
@ -230,17 +232,17 @@ func TestDSLSource(t *testing.T) {
// TestDSLList tests the DSL listing endpoint // TestDSLList tests the DSL listing endpoint
func TestDSLList(t *testing.T) { func TestDSLList(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL List Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL List Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
// Create multiple test models // Create multiple test models
testTag := fmt.Sprintf("test_list_%d", time.Now().UnixNano()) testTag := fmt.Sprintf("test_list_%d", time.Now().UnixNano())
@ -317,17 +319,17 @@ func TestDSLList(t *testing.T) {
// TestDSLUpdate tests the DSL update endpoint // TestDSLUpdate tests the DSL update endpoint
func TestDSLUpdate(t *testing.T) { func TestDSLUpdate(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL Update Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Update Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
testID := fmt.Sprintf("test_update_%d", time.Now().UnixNano()) 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 // TestDSLExists tests the DSL existence check endpoint
func TestDSLExists(t *testing.T) { func TestDSLExists(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL Exists Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Exists Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
testID := fmt.Sprintf("test_exists_%d", time.Now().UnixNano()) testID := fmt.Sprintf("test_exists_%d", time.Now().UnixNano())
nonExistentID := fmt.Sprintf("non_existent_%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 // TestDSLDelete tests the DSL deletion endpoint
func TestDSLDelete(t *testing.T) { func TestDSLDelete(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL Delete Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Delete Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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")
testID := fmt.Sprintf("test_delete_%d", time.Now().UnixNano()) testID := fmt.Sprintf("test_delete_%d", time.Now().UnixNano())
@ -576,17 +578,17 @@ func TestDSLDelete(t *testing.T) {
// TestDSLValidate tests the DSL validation endpoint // TestDSLValidate tests the DSL validation endpoint
func TestDSLValidate(t *testing.T) { func TestDSLValidate(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "DSL Validate Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "DSL Validate Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
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 { tests := []struct {
name string name string
@ -662,12 +664,12 @@ func TestDSLValidate(t *testing.T) {
// TestDSLUnauthorized tests that endpoints return 401 when not authenticated // TestDSLUnauthorized tests that endpoints return 401 when not authenticated
func TestDSLUnauthorized(t *testing.T) { func TestDSLUnauthorized(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
endpoints := []struct { endpoints := []struct {

View file

@ -1,4 +1,4 @@
package openapi package openapi_test
import ( import (
"encoding/json" "encoding/json"
@ -6,17 +6,19 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/tests/testutils"
"github.com/yaoapp/yao/share" "github.com/yaoapp/yao/share"
) )
func TestHelloWorldPublic(t *testing.T) { func TestHelloWorldPublic(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
tests := []struct { tests := []struct {
@ -79,21 +81,21 @@ func TestHelloWorldPublic(t *testing.T) {
} }
func TestHelloWorldProtected(t *testing.T) { func TestHelloWorldProtected(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register a test client for authentication // Register a test client for authentication
client := RegisterTestClient(t, "Hello World Protected Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Hello World Protected Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
// Obtain access token for authentication // 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 { tests := []struct {
name string name string
@ -164,13 +166,13 @@ func TestHelloWorldProtected(t *testing.T) {
} }
func TestHelloWorldProtectedUnauthorized(t *testing.T) { func TestHelloWorldProtectedUnauthorized(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
tests := []struct { tests := []struct {

View file

@ -1,4 +1,4 @@
package openapi package openapi_test
import ( import (
"bytes" "bytes"
@ -9,32 +9,34 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/tests/testutils"
) )
func TestOAuthRegister(t *testing.T) { func TestOAuthRegister(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Debug: Check if Server is properly initialized // Debug: Check if openapi.Server is properly initialized
if Server == nil { if openapi.Server == nil {
t.Fatal("OpenAPI Server is nil") t.Fatal("OpenAPI openapi.Server is nil")
} }
if Server.Config == nil { if openapi.Server.Config == nil {
t.Fatal("OpenAPI Server.Config is nil") t.Fatal("OpenAPI openapi.Server.Config is nil")
} }
if Server.OAuth == nil { if openapi.Server.OAuth == nil {
t.Fatal("OpenAPI Server.OAuth is 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 // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
endpoint := serverURL + baseURL + "/oauth/register" endpoint := serverURL + baseURL + "/oauth/register"
@ -108,24 +110,24 @@ func TestOAuthRegister(t *testing.T) {
assert.Equal(t, req.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs) assert.Equal(t, req.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs)
// Verify that default values were applied when not specified in request // 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.GrantTypes, "openapi.Server should apply default grant types")
assert.NotEmpty(t, response.DynamicClientRegistrationRequest.ResponseTypes, "Server should apply default response types") assert.NotEmpty(t, response.DynamicClientRegistrationRequest.ResponseTypes, "openapi.Server should apply default response types")
assert.Equal(t, "web", response.DynamicClientRegistrationRequest.ApplicationType, "Server should apply default application type") assert.Equal(t, "web", response.DynamicClientRegistrationRequest.ApplicationType, "openapi.Server should apply default application type")
assert.Equal(t, "client_secret_basic", response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod, "Server should apply default auth method") assert.Equal(t, "client_secret_basic", response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod, "openapi.Server should apply default auth method")
} }
}) })
} }
func TestOAuthAuthorize(t *testing.T) { func TestOAuthAuthorize(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Register a test client for realistic testing // Register a test client for realistic testing
testClient := RegisterTestClient(t, "OAuth Test Client", []string{"http://localhost/callback"}) testClient := testutils.RegisterTestClient(t, "OAuth Test Client", []string{"http://localhost/callback"})
defer CleanupTestClient(t, testClient.ClientID) defer testutils.CleanupTestClient(t, testClient.ClientID)
// Prepare test data // testutils.Prepare test data
endpoint := serverURL + Server.Config.BaseURL + "/oauth/authorize" endpoint := serverURL + openapi.Server.Config.BaseURL + "/oauth/authorize"
t.Logf("Testing authorize endpoint: %s", endpoint) t.Logf("Testing authorize endpoint: %s", endpoint)
t.Run("Valid Authorization Request", func(t *testing.T) { t.Run("Valid Authorization Request", func(t *testing.T) {
@ -324,28 +326,28 @@ func TestOAuthAuthorize(t *testing.T) {
} }
func TestOAuthJWKS(t *testing.T) { func TestOAuthJWKS(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Debug: Check if Server is properly initialized // Debug: Check if openapi.Server is properly initialized
if Server == nil { if openapi.Server == nil {
t.Fatal("OpenAPI Server is nil") t.Fatal("OpenAPI openapi.Server is nil")
} }
if Server.Config == nil { if openapi.Server.Config == nil {
t.Fatal("OpenAPI Server.Config is nil") t.Fatal("OpenAPI openapi.Server.Config is nil")
} }
if Server.OAuth == nil { if openapi.Server.OAuth == nil {
t.Fatal("OpenAPI Server.OAuth is 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 // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
endpoint := serverURL + baseURL + "/oauth/jwks" endpoint := serverURL + baseURL + "/oauth/jwks"

View file

@ -1,4 +1,4 @@
package openapi package openapi_test
import ( import (
"bytes" "bytes"
@ -9,29 +9,32 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/oauth/types" "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) { func TestOAuthToken_AuthorizationCode(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register a test client // Register a test client
client := RegisterTestClient(t, "Token Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Token Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
// Obtain authorization code dynamically // 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 // Test authorization code grant
t.Run("Valid Authorization Code Grant", func(t *testing.T) { 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 := url.Values{}
data.Set("grant_type", "authorization_code") data.Set("grant_type", "authorization_code")
data.Set("code", authInfo.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) { t.Run("Invalid Authorization Code", func(t *testing.T) {
// Test with invalid authorization code - should return error // 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 := url.Values{}
data.Set("grant_type", "authorization_code") data.Set("grant_type", "authorization_code")
data.Set("code", "invalid-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) { 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 := url.Values{}
data.Set("grant_type", "authorization_code") data.Set("grant_type", "authorization_code")
data.Set("code", authInfo.Code) data.Set("code", authInfo.Code)
@ -130,21 +133,21 @@ func TestOAuthToken_AuthorizationCode(t *testing.T) {
} }
func TestOAuthToken_ClientCredentials(t *testing.T) { func TestOAuthToken_ClientCredentials(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register a test client for client credentials // Register a test client for client credentials
client := RegisterTestClient(t, "Client Credentials Test", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Client Credentials Test", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
t.Run("Valid Client Credentials Grant", func(t *testing.T) { t.Run("Valid Client Credentials Grant", func(t *testing.T) {
// Prepare token request // testutils.Prepare token request
data := url.Values{} data := url.Values{}
data.Set("grant_type", "client_credentials") data.Set("grant_type", "client_credentials")
data.Set("scope", "api:read api:write") 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) { t.Run("Client Credentials Without Authentication", func(t *testing.T) {
// Prepare token request // testutils.Prepare token request
data := url.Values{} data := url.Values{}
data.Set("grant_type", "client_credentials") data.Set("grant_type", "client_credentials")
@ -213,21 +216,21 @@ func TestOAuthToken_ClientCredentials(t *testing.T) {
} }
func TestOAuthToken_RefreshToken(t *testing.T) { func TestOAuthToken_RefreshToken(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register a test client // Register a test client
client := RegisterTestClient(t, "Refresh Token Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Refresh Token Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
// First, get an access token and refresh token using authorization code // 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 // Get initial token with PKCE code verifier
data := url.Values{} data := url.Values{}
@ -256,7 +259,7 @@ func TestOAuthToken_RefreshToken(t *testing.T) {
assert.NotEmpty(t, initialToken.RefreshToken) assert.NotEmpty(t, initialToken.RefreshToken)
t.Run("Valid Refresh Token Grant", func(t *testing.T) { t.Run("Valid Refresh Token Grant", func(t *testing.T) {
// Prepare refresh token request // testutils.Prepare refresh token request
data := url.Values{} data := url.Values{}
data.Set("grant_type", "refresh_token") data.Set("grant_type", "refresh_token")
data.Set("refresh_token", initialToken.RefreshToken) 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) { 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 := url.Values{}
data.Set("grant_type", "refresh_token") data.Set("grant_type", "refresh_token")
data.Set("refresh_token", "invalid-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) { 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 := url.Values{}
data.Set("grant_type", "refresh_token") data.Set("grant_type", "refresh_token")
// Missing refresh_token // Missing refresh_token
@ -351,20 +354,20 @@ func TestOAuthToken_RefreshToken(t *testing.T) {
} }
func TestOAuthToken_InvalidGrantType(t *testing.T) { func TestOAuthToken_InvalidGrantType(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
client := RegisterTestClient(t, "Invalid Grant Test", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Invalid Grant Test", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
t.Run("Unsupported Grant Type", func(t *testing.T) { 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 := url.Values{}
data.Set("grant_type", "unsupported_grant_type") 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) { 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{} data := url.Values{}
// Missing grant_type // Missing grant_type
@ -462,24 +465,24 @@ func base64Encode(data []byte) string {
} }
func TestOAuthRevoke(t *testing.T) { func TestOAuthRevoke(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register a test client // Register a test client
client := RegisterTestClient(t, "Revoke Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Revoke Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
// Obtain access token directly using the utility function // 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) { t.Run("Valid Access Token Revocation", func(t *testing.T) {
// Prepare revocation request // testutils.Prepare revocation request
data := url.Values{} data := url.Values{}
data.Set("token", tokenInfo.AccessToken) data.Set("token", tokenInfo.AccessToken)
data.Set("token_type_hint", "access_token") 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) { 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 := url.Values{}
data.Set("token", tokenInfo.RefreshToken) data.Set("token", tokenInfo.RefreshToken)
data.Set("token_type_hint", "refresh_token") 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) { 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 := url.Values{}
data.Set("token", "invalid-token-12345") data.Set("token", "invalid-token-12345")
data.Set("token_type_hint", "access_token") 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) { 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{} data := url.Values{}
// Missing token parameter // Missing token parameter
@ -573,24 +576,24 @@ func TestOAuthRevoke(t *testing.T) {
} }
func TestOAuthIntrospect(t *testing.T) { func TestOAuthIntrospect(t *testing.T) {
serverURL := Prepare(t) serverURL := testutils.Prepare(t)
defer Clean() defer testutils.Clean()
// Get base URL from server config // Get base URL from server config
baseURL := "" baseURL := ""
if Server != nil && Server.Config != nil { if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = Server.Config.BaseURL baseURL = openapi.Server.Config.BaseURL
} }
// Register a test client // Register a test client
client := RegisterTestClient(t, "Introspect Test Client", []string{"https://localhost/callback"}) client := testutils.RegisterTestClient(t, "Introspect Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID) defer testutils.CleanupTestClient(t, client.ClientID)
// Obtain access token directly using the utility function // 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) { t.Run("Valid Access Token Introspection", func(t *testing.T) {
// Prepare introspection request // testutils.Prepare introspection request
data := url.Values{} data := url.Values{}
data.Set("token", tokenInfo.AccessToken) data.Set("token", tokenInfo.AccessToken)
data.Set("token_type_hint", "access_token") data.Set("token_type_hint", "access_token")
@ -615,7 +618,7 @@ func TestOAuthIntrospect(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Parse response directly (no wrapper) // Parse response directly (no wrapper)
var introspectResp TokenIntrospectionResponse var introspectResp response.TokenIntrospectionResponse
err = json.Unmarshal(body, &introspectResp) err = json.Unmarshal(body, &introspectResp)
assert.NoError(t, err) assert.NoError(t, err)
@ -630,7 +633,7 @@ func TestOAuthIntrospect(t *testing.T) {
}) })
t.Run("Invalid Token Introspection", func(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 := url.Values{}
data.Set("token", "invalid-token-12345") data.Set("token", "invalid-token-12345")
data.Set("token_type_hint", "access_token") data.Set("token_type_hint", "access_token")
@ -655,7 +658,7 @@ func TestOAuthIntrospect(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Parse response directly (no wrapper) // Parse response directly (no wrapper)
var introspectResp TokenIntrospectionResponse var introspectResp response.TokenIntrospectionResponse
err = json.Unmarshal(body, &introspectResp) err = json.Unmarshal(body, &introspectResp)
assert.NoError(t, err) assert.NoError(t, err)
@ -666,7 +669,7 @@ func TestOAuthIntrospect(t *testing.T) {
}) })
t.Run("Missing Token Parameter", func(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{} data := url.Values{}
// Missing token parameter // Missing token parameter
@ -727,7 +730,7 @@ func TestOAuthIntrospect(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Parse response directly (no wrapper) // Parse response directly (no wrapper)
var introspectResp TokenIntrospectionResponse var introspectResp response.TokenIntrospectionResponse
err = json.Unmarshal(body, &introspectResp) err = json.Unmarshal(body, &introspectResp)
assert.NoError(t, err) assert.NoError(t, err)

View file

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

View file

@ -1,4 +1,4 @@
package openapi package testutils
import ( import (
"context" "context"
@ -8,12 +8,13 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"sync"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/config" "github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/test" "github.com/yaoapp/yao/test"
) )
@ -21,6 +22,12 @@ import (
// testServer holds the test HTTP server instance // testServer holds the test HTTP server instance
var testServer *http.Server 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. // Prepare initializes the OpenAPI test environment and starts a mock HTTP server.
// //
// AI ASSISTANT INSTRUCTIONS: // AI ASSISTANT INSTRUCTIONS:
@ -85,13 +92,22 @@ var testServer *http.Server
// ERROR HANDLING: // ERROR HANDLING:
// If any step fails, the test will fail immediately with a descriptive error message. // If any step fails, the test will fail immediately with a descriptive error message.
func Prepare(t *testing.T) string { 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 // Step 1: Initialize base test environment with all Yao dependencies
test.Prepare(t, config.Conf) test.Prepare(t, config.Conf)
// Step 2: Initialize OpenAPI server and make it available globally // Step 2: Initialize OpenAPI server and make it available globally (only if not already initialized)
_, err := Load(config.Conf) if openapi.Server == nil {
if err != nil { _, err := openapi.Load(config.Conf)
t.Fatalf("Failed to load OpenAPI server: %v", err) if err != nil {
t.Fatalf("Failed to load OpenAPI server: %v", err)
}
} }
// Step 3: Create Gin router and attach OpenAPI server // Step 3: Create Gin router and attach OpenAPI server
@ -99,8 +115,8 @@ func Prepare(t *testing.T) string {
router := gin.New() router := gin.New()
// Attach the OpenAPI server to the router // Attach the OpenAPI server to the router
if Server != nil { if openapi.Server != nil {
Server.Attach(router) openapi.Server.Attach(router)
} }
// Step 4: Start HTTP server on random available port // 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) t.Fatalf("Failed to create listener: %v", err)
} }
testServer = &http.Server{ server := &http.Server{
Handler: router, Handler: router,
} }
// Start server in background // Start server in background
go func() { 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) 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 // Wait a moment for server to start
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
// Store server instance for this test (each test gets its own HTTP server)
testServer = server
// Return server URL // Return server URL
serverURL := fmt.Sprintf("http://%s", listener.Addr().String()) serverURL := fmt.Sprintf("http://%s", listener.Addr().String())
return serverURL 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 // - 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 // - Server shutdown has a 5-second timeout to prevent hanging tests
func Clean() { func Clean() {
// Step 1: Gracefully shutdown the HTTP test server // Step 1: Gracefully shutdown the HTTP test server for this test
if testServer != nil { if testServer != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -166,11 +185,21 @@ func Clean() {
testServer = nil testServer = nil
} }
// Step 2: Reset OpenAPI server instance to prevent state leakage // Step 2: Use lock to safely decrement active test count and clean global state if needed
Server = nil 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 // Step 3: Clean up base test environment
test.Clean() if shouldCleanGlobalState {
test.Clean()
}
} }
// RegisterTestClient registers a test OAuth client and returns the client information. // RegisterTestClient registers a test OAuth client and returns the client information.
@ -209,7 +238,11 @@ func Clean() {
// ERROR HANDLING: // ERROR HANDLING:
// If client registration fails, the test will fail immediately with a descriptive error message. // 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 { 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.") 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 // Register the client using the OAuth service
ctx := context.Background() ctx := context.Background()
response, err := Server.OAuth.DynamicClientRegistration(ctx, req) response, err := server.OAuth.DynamicClientRegistration(ctx, req)
if err != nil { if err != nil {
t.Fatalf("Failed to register test client: %v", err) 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. // If client deletion fails, logs an error but does not fail the test.
// This prevents cleanup failures from affecting test results. // This prevents cleanup failures from affecting test results.
func CleanupTestClient(t *testing.T, clientID string) { 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 // Server might already be cleaned up, which is OK
return return
} }
@ -290,7 +327,7 @@ func CleanupTestClient(t *testing.T, clientID string) {
// Delete the client using the OAuth service // Delete the client using the OAuth service
ctx := context.Background() ctx := context.Background()
err := Server.OAuth.DeleteClient(ctx, clientID) err := server.OAuth.DeleteClient(ctx, clientID)
if err != nil { if err != nil {
// Log error but don't fail the test - cleanup should be resilient // Log error but don't fail the test - cleanup should be resilient
t.Logf("Warning: Failed to cleanup test client %s: %v", clientID, err) 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" 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. // ObtainAuthorizationCode dynamically obtains an authorization code for testing OAuth token endpoints.
// //
// AI ASSISTANT INSTRUCTIONS: // AI ASSISTANT INSTRUCTIONS:
@ -383,8 +421,13 @@ type AuthorizationInfo struct {
CodeChallengeMethod string CodeChallengeMethod string
} }
// ObtainAuthorizationCode obtains an authorization code for testing OAuth token endpoints.
func ObtainAuthorizationCode(t *testing.T, serverURL, clientID, redirectURI, scope string) *AuthorizationInfo { 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.") 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 // Call OAuth service to process authorization request
ctx := context.Background() ctx := context.Background()
authResp, err := Server.OAuth.Authorize(ctx, authReq) authResp, err := server.OAuth.Authorize(ctx, authReq)
if err != nil { if err != nil {
t.Fatalf("Failed to obtain authorization code: %v", err) t.Fatalf("Failed to obtain authorization code: %v", err)
} }
@ -439,6 +482,7 @@ func ObtainAuthorizationCode(t *testing.T, serverURL, clientID, redirectURI, sco
return authInfo return authInfo
} }
// TokenInfo represents the information needed for OAuth token exchange.
// ObtainAccessToken directly obtains an access token for testing OAuth endpoints that require authentication. // ObtainAccessToken directly obtains an access token for testing OAuth endpoints that require authentication.
// //
// AI ASSISTANT INSTRUCTIONS: // AI ASSISTANT INSTRUCTIONS:
@ -495,8 +539,13 @@ type TokenInfo struct {
ClientID string 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 { 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.") 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 // Step 2: Exchange authorization code for access token with PKCE code verifier
ctx := context.Background() 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 { if err != nil {
t.Fatalf("Failed to exchange authorization code for token: %v", err) 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 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 // generateCodeVerifier generates a cryptographically random code verifier for PKCE
func generateCodeVerifier() string { func generateCodeVerifier() string {
// PKCE code verifier should be 43-128 characters long // PKCE code verifier should be 43-128 characters long