mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
46edaf2112
Consolidates coderdtest invocations in 7 tests to reduce 23 instances to 7 across: - `TestGetUser` (3 → 1) — read-only user lookups - `TestUserTerminalFont` (3 → 1) — each creates own user via CreateAnotherUser - `TestUserTaskNotificationAlertDismissed` (3 → 1) — each creates own user - `TestUserLogin` (3 → 1) — each creates/deletes own user - `TestExpMcpConfigureClaudeCode` (5 → 1) — writes to isolated temp dirs - `TestOAuth2RegistrationTokenSecurity` (3 → 1) — independent registrations - `TestOAuth2SpecificErrorScenarios` (3 → 1) — independent error scenarios > 🤖 This PR was created with the help of Coder Agents, and has been reviewed by my human. 🧑💻
432 lines
14 KiB
Go
432 lines
14 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
// OAuth2ErrorResponse represents RFC-compliant OAuth2 error responses
|
|
type OAuth2ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
ErrorDescription string `json:"error_description,omitempty"`
|
|
ErrorURI string `json:"error_uri,omitempty"`
|
|
}
|
|
|
|
// TestOAuth2ErrorResponseFormat tests that OAuth2 error responses follow proper RFC format
|
|
func TestOAuth2ErrorResponseFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ContentTypeHeader", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Make a request that will definitely fail
|
|
req := codersdk.OAuth2ClientRegistrationRequest{
|
|
// Missing required redirect_uris
|
|
}
|
|
|
|
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
|
require.Error(t, err)
|
|
|
|
// Check that it's an HTTP error with JSON content type
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
|
|
// The error should be a 400 status for invalid client metadata
|
|
require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
// TestOAuth2RegistrationErrorCodes tests all RFC 7591 error codes
|
|
func TestOAuth2RegistrationErrorCodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
req codersdk.OAuth2ClientRegistrationRequest
|
|
expectedError string
|
|
expectedCode int
|
|
}{
|
|
{
|
|
name: "InvalidClientMetadata_NoRedirectURIs",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
// Missing required redirect_uris
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_InvalidRedirectURI",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"not-a-valid-uri"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_RedirectURIWithFragment",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback#fragment"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_HTTPRedirectForNonLocalhost",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"http://example.com/callback"}, // HTTP for non-localhost
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_UnsupportedGrantType",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
GrantTypes: []codersdk.OAuth2ProviderGrantType{"unsupported_grant_type"},
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_UnsupportedResponseType",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
ResponseTypes: []codersdk.OAuth2ProviderResponseType{"unsupported_response_type"},
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_UnsupportedAuthMethod",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
TokenEndpointAuthMethod: "unsupported_auth_method",
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_InvalidClientURI",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
ClientURI: "not-a-valid-uri",
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "InvalidClientMetadata_InvalidLogoURI",
|
|
req: codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
LogoURI: "not-a-valid-uri",
|
|
},
|
|
expectedError: "invalid_client_metadata",
|
|
expectedCode: http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Create a copy of the request with a unique client name
|
|
req := test.req
|
|
if req.ClientName != "" {
|
|
req.ClientName = fmt.Sprintf("%s-%d", req.ClientName, time.Now().UnixNano())
|
|
}
|
|
|
|
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
|
require.Error(t, err)
|
|
|
|
// Validate error format and status code
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, test.expectedCode, httpErr.StatusCode())
|
|
|
|
// For now, just verify we get an error with the expected status code
|
|
// The specific error message format can be verified in other ways
|
|
require.True(t, httpErr.StatusCode() >= 400)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOAuth2ManagementErrorCodes tests all RFC 7592 error codes
|
|
func TestOAuth2ManagementErrorCodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
useWrongClientID bool
|
|
useWrongToken bool
|
|
useEmptyToken bool
|
|
expectedError string
|
|
expectedCode int
|
|
}{
|
|
{
|
|
name: "InvalidToken_WrongToken",
|
|
useWrongToken: true,
|
|
expectedError: "invalid_token",
|
|
expectedCode: http.StatusUnauthorized,
|
|
},
|
|
{
|
|
name: "InvalidToken_EmptyToken",
|
|
useEmptyToken: true,
|
|
expectedError: "invalid_token",
|
|
expectedCode: http.StatusUnauthorized,
|
|
},
|
|
{
|
|
name: "InvalidClient_WrongClientID",
|
|
useWrongClientID: true,
|
|
expectedError: "invalid_token",
|
|
expectedCode: http.StatusUnauthorized,
|
|
},
|
|
// Skip empty client ID test as it causes routing issues
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// First register a valid client to use for management tests
|
|
clientName := fmt.Sprintf("test-client-%d", time.Now().UnixNano())
|
|
regReq := codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: clientName,
|
|
}
|
|
regResp, err := client.PostOAuth2ClientRegistration(ctx, regReq)
|
|
require.NoError(t, err)
|
|
|
|
// Determine clientID and token based on test configuration
|
|
var clientID, token string
|
|
switch {
|
|
case test.useWrongClientID:
|
|
clientID = "550e8400-e29b-41d4-a716-446655440000" // Valid UUID format but non-existent
|
|
token = regResp.RegistrationAccessToken
|
|
case test.useWrongToken:
|
|
clientID = regResp.ClientID
|
|
token = "invalid-token"
|
|
case test.useEmptyToken:
|
|
clientID = regResp.ClientID
|
|
token = ""
|
|
default:
|
|
clientID = regResp.ClientID
|
|
token = regResp.RegistrationAccessToken
|
|
}
|
|
|
|
// Test GET client configuration
|
|
_, err = client.GetOAuth2ClientConfiguration(ctx, clientID, token)
|
|
require.Error(t, err)
|
|
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, test.expectedCode, httpErr.StatusCode())
|
|
// Verify we get an appropriate error status code
|
|
require.True(t, httpErr.StatusCode() >= 400)
|
|
|
|
// Test PUT client configuration (except for empty client ID which causes routing issues)
|
|
if clientID != "" {
|
|
updateReq := codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://updated.example.com/callback"},
|
|
ClientName: clientName + "-updated",
|
|
}
|
|
_, err = client.PutOAuth2ClientConfiguration(ctx, clientID, token, updateReq)
|
|
require.Error(t, err)
|
|
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, test.expectedCode, httpErr.StatusCode())
|
|
require.True(t, httpErr.StatusCode() >= 400)
|
|
|
|
// Test DELETE client configuration
|
|
err = client.DeleteOAuth2ClientConfiguration(ctx, clientID, token)
|
|
require.Error(t, err)
|
|
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, test.expectedCode, httpErr.StatusCode())
|
|
require.True(t, httpErr.StatusCode() >= 400)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOAuth2ErrorResponseStructure tests the JSON structure of error responses
|
|
func TestOAuth2ErrorResponseStructure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ErrorFieldsPresent", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Make a request that will generate an error
|
|
req := codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"invalid-uri"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
}
|
|
|
|
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
|
require.Error(t, err)
|
|
|
|
// Validate that the error contains the expected OAuth2 error structure
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
|
|
// The error should be a 400 status for invalid client metadata
|
|
require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
|
|
|
|
// Should have error details
|
|
require.NotEmpty(t, httpErr.Message)
|
|
})
|
|
|
|
t.Run("RegistrationAccessTokenErrors", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Try to access a client configuration with invalid token - use a valid UUID format
|
|
validUUID := "550e8400-e29b-41d4-a716-446655440000"
|
|
_, err := client.GetOAuth2ClientConfiguration(ctx, validUUID, "invalid-token")
|
|
require.Error(t, err)
|
|
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, http.StatusUnauthorized, httpErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
// TestOAuth2ErrorHTTPHeaders tests that error responses have correct HTTP headers
|
|
func TestOAuth2ErrorHTTPHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ContentTypeJSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Make a request that will fail
|
|
req := codersdk.OAuth2ClientRegistrationRequest{
|
|
// Missing required fields
|
|
}
|
|
|
|
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
|
require.Error(t, err)
|
|
|
|
// The error should indicate proper JSON response format
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.NotEmpty(t, httpErr.Message)
|
|
})
|
|
}
|
|
|
|
// TestOAuth2SpecificErrorScenarios tests specific error scenarios from RFC specifications
|
|
func TestOAuth2SpecificErrorScenarios(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Single instance shared across all sub-tests that need a
|
|
// coderd server. Sub-tests that don't need one just ignore it.
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
t.Run("MissingRequiredFields", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Test completely empty request
|
|
req := codersdk.OAuth2ClientRegistrationRequest{}
|
|
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
|
require.Error(t, err)
|
|
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
|
|
// Error properly returned with bad request status
|
|
})
|
|
|
|
t.Run("InvalidJSONStructure", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// For invalid JSON structure, we'd need to make raw HTTP requests
|
|
// This is tested implicitly through the other tests since we're using
|
|
// typed requests that ensure proper JSON structure
|
|
})
|
|
|
|
t.Run("UnsupportedFields", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Test with fields that might not be supported yet
|
|
req := codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
|
TokenEndpointAuthMethod: "private_key_jwt", // Not supported yet
|
|
}
|
|
|
|
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
|
require.Error(t, err)
|
|
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
|
|
// Error properly returned with bad request status
|
|
})
|
|
|
|
t.Run("SecurityBoundaryErrors", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Register a client first
|
|
clientName := fmt.Sprintf("test-client-%d", time.Now().UnixNano())
|
|
regReq := codersdk.OAuth2ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: clientName,
|
|
}
|
|
regResp, err := client.PostOAuth2ClientRegistration(ctx, regReq)
|
|
require.NoError(t, err)
|
|
|
|
// Try to access with completely wrong token format
|
|
_, err = client.GetOAuth2ClientConfiguration(ctx, regResp.ClientID, "malformed-token-format")
|
|
require.Error(t, err)
|
|
|
|
var httpErr *codersdk.Error
|
|
require.ErrorAs(t, err, &httpErr)
|
|
require.Equal(t, http.StatusUnauthorized, httpErr.StatusCode())
|
|
})
|
|
}
|