yao/openapi/oauth_test.go

324 lines
11 KiB
Go

package openapi
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi/oauth/types"
)
func TestOAuthRegister(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
// Debug: Check if Server is properly initialized
if Server == nil {
t.Fatal("OpenAPI Server is nil")
}
if Server.Config == nil {
t.Fatal("OpenAPI Server.Config is nil")
}
if Server.OAuth == nil {
t.Fatal("OpenAPI Server.OAuth is nil")
}
t.Logf("Server initialized with BaseURL: %s", Server.Config.BaseURL)
// Get base URL from server config
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
endpoint := serverURL + baseURL + "/oauth/register"
t.Logf("Testing endpoint: %s", endpoint)
t.Run("Valid Client Registration", func(t *testing.T) {
// Minimal valid registration request to isolate the issue
req := types.DynamicClientRegistrationRequest{
RedirectURIs: []string{
"http://localhost/callback",
},
ClientName: "Test Client",
}
// Convert to JSON
jsonData, err := json.Marshal(req)
assert.NoError(t, err)
t.Logf("Request JSON: %s", string(jsonData))
// Make POST request
t.Logf("Making POST request to: %s", endpoint)
resp, err := http.Post(endpoint, "application/json", bytes.NewBuffer(jsonData))
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
t.Logf("Response status code: %d", resp.StatusCode)
// Verify OAuth 2.1 security headers are present
assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"), "Cache-Control header should be set")
assert.Equal(t, "no-cache", resp.Header.Get("Pragma"), "Pragma header should be set")
assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"), "X-Content-Type-Options header should be set")
assert.Equal(t, "DENY", resp.Header.Get("X-Frame-Options"), "X-Frame-Options header should be set")
assert.Equal(t, "no-referrer", resp.Header.Get("Referrer-Policy"), "Referrer-Policy header should be set")
assert.Equal(t, "application/json;charset=UTF-8", resp.Header.Get("Content-Type"), "Content-Type header should be set")
// Read the complete response body for debugging
bodyBytes, _ := io.ReadAll(resp.Body)
t.Logf("Complete response body: %s", string(bodyBytes))
// Reset the response body for JSON decoding
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
// Check status code
if resp.StatusCode != http.StatusCreated {
// Read error response for debugging
body, _ := io.ReadAll(resp.Body)
t.Logf("Expected 201, got %d. Response: %s", resp.StatusCode, string(body))
}
assert.Equal(t, http.StatusCreated, resp.StatusCode)
// Parse response
t.Logf("Parsing response body...")
var response types.DynamicClientRegistrationResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
t.Logf("Failed to decode response: %v", err)
}
assert.NoError(t, err)
t.Logf("Response ClientID: %s", response.ClientID)
t.Logf("Response ClientSecret: %s", response.ClientSecret)
// Verify response contains generated client credentials
assert.NotEmpty(t, response.ClientID)
assert.NotEmpty(t, response.ClientSecret)
// Verify request data is preserved in response
if response.DynamicClientRegistrationRequest != nil {
assert.Equal(t, req.ClientName, response.DynamicClientRegistrationRequest.ClientName)
assert.Equal(t, req.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs)
// Verify that default values were applied when not specified in request
assert.NotEmpty(t, response.DynamicClientRegistrationRequest.GrantTypes, "Server should apply default grant types")
assert.NotEmpty(t, response.DynamicClientRegistrationRequest.ResponseTypes, "Server should apply default response types")
assert.Equal(t, "web", response.DynamicClientRegistrationRequest.ApplicationType, "Server should apply default application type")
assert.Equal(t, "client_secret_basic", response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod, "Server should apply default auth method")
}
})
}
func TestOAuthAuthorize(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
// Register a test client for realistic testing
testClient := RegisterTestClient(t, "OAuth Test Client", []string{"http://localhost/callback"})
defer CleanupTestClient(t, testClient.ClientID)
// Prepare test data
endpoint := serverURL + Server.Config.BaseURL + "/oauth/authorize"
t.Logf("Testing authorize endpoint: %s", endpoint)
t.Run("Valid Authorization Request", func(t *testing.T) {
// Test valid authorization request with real client
params := url.Values{}
params.Set("client_id", testClient.ClientID) // Use real registered client ID
params.Set("response_type", "code")
params.Set("redirect_uri", testClient.RedirectURIs[0]) // Use registered redirect URI
params.Set("scope", "openid profile")
params.Set("state", "test-state-123")
requestURL := endpoint + "?" + params.Encode()
t.Logf("Making GET request to: %s", requestURL)
// Configure HTTP client to not follow redirects automatically
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(requestURL)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
t.Logf("Response status code: %d", resp.StatusCode)
// Should redirect with either success (302) or error (302)
assert.Equal(t, http.StatusFound, resp.StatusCode)
// Check redirect location
location := resp.Header.Get("Location")
assert.NotEmpty(t, location, "Location header should be present")
t.Logf("Redirect location: %s", location)
// Parse redirect URL to check parameters
redirectURL, err := url.Parse(location)
assert.NoError(t, err)
// Should contain either 'code' (success) or 'error' (failure) parameter
query := redirectURL.Query()
hasCode := query.Get("code") != ""
hasError := query.Get("error") != ""
assert.True(t, hasCode || hasError, "Redirect should contain either 'code' or 'error' parameter")
// State parameter should be preserved
assert.Equal(t, "test-state-123", query.Get("state"), "State parameter should be preserved")
t.Logf("Authorization result - Code: %s, Error: %s", query.Get("code"), query.Get("error"))
})
t.Run("Invalid Client ID", func(t *testing.T) {
// Test with invalid client ID
params := url.Values{}
params.Set("client_id", "invalid-client-id")
params.Set("response_type", "code")
params.Set("redirect_uri", "http://localhost/callback")
params.Set("scope", "openid profile")
params.Set("state", "test-state-456")
requestURL := endpoint + "?" + params.Encode()
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(requestURL)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusFound, resp.StatusCode)
location := resp.Header.Get("Location")
redirectURL, err := url.Parse(location)
assert.NoError(t, err)
query := redirectURL.Query()
assert.Equal(t, "invalid_client", query.Get("error"), "Should return invalid_client error")
assert.Equal(t, "test-state-456", query.Get("state"), "State should be preserved")
})
t.Run("Valid Authorization Request via POST", func(t *testing.T) {
// Test valid authorization request with POST method
form := url.Values{}
form.Set("client_id", testClient.ClientID)
form.Set("response_type", "code")
form.Set("redirect_uri", testClient.RedirectURIs[0])
form.Set("scope", "openid profile")
form.Set("state", "test-post-state-789")
t.Logf("Making POST request to: %s", endpoint)
// Configure HTTP client to not follow redirects automatically
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.PostForm(endpoint, form)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
t.Logf("Response status code: %d", resp.StatusCode)
// Should redirect with either success (302) or error (302)
assert.Equal(t, http.StatusFound, resp.StatusCode)
// Check redirect location
location := resp.Header.Get("Location")
assert.NotEmpty(t, location, "Location header should be present")
t.Logf("Redirect location: %s", location)
// Parse redirect URL to check parameters
redirectURL, err := url.Parse(location)
assert.NoError(t, err)
// Should contain either 'code' (success) or 'error' (failure) parameter
query := redirectURL.Query()
hasCode := query.Get("code") != ""
hasError := query.Get("error") != ""
assert.True(t, hasCode || hasError, "Redirect should contain either 'code' or 'error' parameter")
// State parameter should be preserved
assert.Equal(t, "test-post-state-789", query.Get("state"), "State parameter should be preserved")
t.Logf("Authorization result (POST) - Code: %s, Error: %s", query.Get("code"), query.Get("error"))
})
t.Run("Invalid Response Type via POST", func(t *testing.T) {
// Test with invalid response type using POST
form := url.Values{}
form.Set("client_id", testClient.ClientID)
form.Set("response_type", "token") // Implicit flow - deprecated in OAuth 2.1
form.Set("redirect_uri", testClient.RedirectURIs[0])
form.Set("scope", "openid profile")
form.Set("state", "test-invalid-response-type")
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.PostForm(endpoint, form)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusFound, resp.StatusCode)
location := resp.Header.Get("Location")
redirectURL, err := url.Parse(location)
assert.NoError(t, err)
query := redirectURL.Query()
assert.Equal(t, "unsupported_response_type", query.Get("error"), "Should return unsupported_response_type error")
assert.Equal(t, "test-invalid-response-type", query.Get("state"), "State should be preserved")
})
t.Run("Missing Required Parameters via POST", func(t *testing.T) {
// Test with missing client_id using POST
form := url.Values{}
// Missing client_id
form.Set("response_type", "code")
form.Set("redirect_uri", testClient.RedirectURIs[0])
form.Set("scope", "openid profile")
form.Set("state", "test-missing-client-id")
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.PostForm(endpoint, form)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusFound, resp.StatusCode)
location := resp.Header.Get("Location")
redirectURL, err := url.Parse(location)
assert.NoError(t, err)
query := redirectURL.Query()
assert.Equal(t, "invalid_request", query.Get("error"), "Should return invalid_request error")
assert.Equal(t, "test-missing-client-id", query.Get("state"), "State should be preserved")
})
}