refactor: organize OAuth2 provider tests into dedicated packages (#18747)

# OAuth2 Provider Code Reorganization

This PR reorganizes the OAuth2 provider code to improve separation of concerns and maintainability. The changes include:

1. Migrating OAuth2 provider app validation tests from `coderd/oauth2_test.go` to `oauth2provider/provider_test.go`
2. Moving OAuth2 client registration validation tests to `oauth2provider/validation_test.go`
3. Adding new comprehensive test files for metadata and validation edge cases
4. Renaming `OAuth2ProviderAppSecret` to `AppSecret` for better naming consistency
5. Simplifying the main integration test in `oauth2_test.go` to focus on core functionality

The PR maintains all existing test coverage while organizing the code more logically, making it easier to understand and maintain the OAuth2 provider implementation. This reorganization will help with future enhancements to the OAuth2 provider functionality.
This commit is contained in:
Thomas Kosiewski
2025-07-03 20:41:47 +02:00
committed by GitHub
parent c65013384a
commit 4607e5113b
4 changed files with 1336 additions and 415 deletions

View File

@ -32,287 +32,27 @@ import (
func TestOAuth2ProviderApps(t *testing.T) {
t.Parallel()
t.Run("Validation", func(t *testing.T) {
// NOTE: Unit tests for OAuth2 provider app validation have been migrated to
// oauth2provider/provider_test.go for better separation of concerns.
// This test function now focuses on integration testing with the full server stack.
t.Run("IntegrationFlow", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
tests := []struct {
name string
req codersdk.PostOAuth2ProviderAppRequest
}{
{
name: "NameMissing",
req: codersdk.PostOAuth2ProviderAppRequest{
CallbackURL: "http://localhost:3000",
},
},
{
name: "NameSpaces",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo bar",
CallbackURL: "http://localhost:3000",
},
},
{
name: "NameTooLong",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "too loooooooooooooooooooooooooong",
CallbackURL: "http://localhost:3000",
},
},
{
name: "URLMissing",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
},
},
{
name: "URLLocalhostNoScheme",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "localhost:3000",
},
},
{
name: "URLNoScheme",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "coder.com",
},
},
{
name: "URLNoColon",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "http//coder",
},
},
{
name: "URLJustBar",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "bar",
},
},
{
name: "URLPathOnly",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "/bar/baz/qux",
},
},
{
name: "URLJustHttp",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "http",
},
},
{
name: "URLNoHost",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "http://",
},
},
{
name: "URLSpaces",
req: codersdk.PostOAuth2ProviderAppRequest{
Name: "foo",
CallbackURL: "bar baz qux",
},
},
}
// Generate an application for testing PUTs.
req := codersdk.PostOAuth2ProviderAppRequest{
Name: fmt.Sprintf("quark-%d", time.Now().UnixNano()%1000000),
CallbackURL: "http://coder.com",
}
//nolint:gocritic // OAauth2 app management requires owner permission.
existingApp, err := client.PostOAuth2ProviderApp(ctx, req)
require.NoError(t, err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // OAauth2 app management requires owner permission.
_, err := client.PostOAuth2ProviderApp(ctx, test.req)
require.Error(t, err)
//nolint:gocritic // OAauth2 app management requires owner permission.
_, err = client.PutOAuth2ProviderApp(ctx, existingApp.ID, codersdk.PutOAuth2ProviderAppRequest{
Name: test.req.Name,
CallbackURL: test.req.CallbackURL,
})
require.Error(t, err)
})
}
})
t.Run("DeleteNonExisting", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
_, err := another.OAuth2ProviderApp(ctx, uuid.New())
require.Error(t, err)
})
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
// No apps yet.
apps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
require.NoError(t, err)
require.Len(t, apps, 0)
// Should be able to add apps.
expected := generateApps(ctx, t, client, "get-apps")
expectedOrder := []codersdk.OAuth2ProviderApp{
expected.Default, expected.NoPort,
expected.Extra[0], expected.Extra[1], expected.Subdomain,
}
// Should get all the apps now.
apps, err = another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
require.NoError(t, err)
require.Len(t, apps, 5)
require.Equal(t, expectedOrder, apps)
// Should be able to keep the same name when updating.
req := codersdk.PutOAuth2ProviderAppRequest{
Name: expected.Default.Name,
CallbackURL: "http://coder.com",
Icon: "test",
}
//nolint:gocritic // OAauth2 app management requires owner permission.
newApp, err := client.PutOAuth2ProviderApp(ctx, expected.Default.ID, req)
require.NoError(t, err)
require.Equal(t, req.Name, newApp.Name)
require.Equal(t, req.CallbackURL, newApp.CallbackURL)
require.Equal(t, req.Icon, newApp.Icon)
require.Equal(t, expected.Default.ID, newApp.ID)
// Should be able to update name.
req = codersdk.PutOAuth2ProviderAppRequest{
Name: "new-foo",
CallbackURL: "http://coder.com",
Icon: "test",
}
//nolint:gocritic // OAauth2 app management requires owner permission.
newApp, err = client.PutOAuth2ProviderApp(ctx, expected.Default.ID, req)
require.NoError(t, err)
require.Equal(t, req.Name, newApp.Name)
require.Equal(t, req.CallbackURL, newApp.CallbackURL)
require.Equal(t, req.Icon, newApp.Icon)
require.Equal(t, expected.Default.ID, newApp.ID)
// Should be able to get a single app.
got, err := another.OAuth2ProviderApp(ctx, expected.Default.ID)
require.NoError(t, err)
require.Equal(t, newApp, got)
// Should be able to delete an app.
//nolint:gocritic // OAauth2 app management requires owner permission.
err = client.DeleteOAuth2ProviderApp(ctx, expected.Default.ID)
require.NoError(t, err)
// Should show the new count.
newApps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
require.NoError(t, err)
require.Len(t, newApps, 4)
require.Equal(t, expectedOrder[1:], newApps)
})
t.Run("ByUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
another, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
_ = generateApps(ctx, t, client, "by-user")
apps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{
UserID: user.ID,
// Test basic app creation and management in integration context
//nolint:gocritic // OAuth2 app management requires owner permission.
app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
Name: fmt.Sprintf("integration-test-%d", time.Now().UnixNano()%1000000),
CallbackURL: "http://localhost:3000",
})
require.NoError(t, err)
require.Len(t, apps, 0)
})
t.Run("DuplicateNames", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
// Create multiple OAuth2 apps with the same name to verify RFC 7591 compliance
// RFC 7591 allows multiple apps to have the same name
appName := fmt.Sprintf("duplicate-name-%d", time.Now().UnixNano()%1000000)
// Create first app
//nolint:gocritic // OAuth2 app management requires owner permission.
app1, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
Name: appName,
CallbackURL: "http://localhost:3001",
})
require.NoError(t, err)
require.Equal(t, appName, app1.Name)
// Create second app with the same name
//nolint:gocritic // OAuth2 app management requires owner permission.
app2, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
Name: appName,
CallbackURL: "http://localhost:3002",
})
require.NoError(t, err)
require.Equal(t, appName, app2.Name)
// Create third app with the same name
//nolint:gocritic // OAuth2 app management requires owner permission.
app3, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
Name: appName,
CallbackURL: "http://localhost:3003",
})
require.NoError(t, err)
require.Equal(t, appName, app3.Name)
// Verify all apps have different IDs but same name
require.NotEqual(t, app1.ID, app2.ID)
require.NotEqual(t, app1.ID, app3.ID)
require.NotEqual(t, app2.ID, app3.ID)
require.Equal(t, app1.Name, app2.Name)
require.Equal(t, app1.Name, app3.Name)
// Verify all apps can be retrieved and have the same name
//nolint:gocritic // OAuth2 app management requires owner permission.
apps, err := client.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
require.NoError(t, err)
// Count apps with our duplicate name
duplicateNameCount := 0
for _, app := range apps {
if app.Name == appName {
duplicateNameCount++
}
}
require.Equal(t, 3, duplicateNameCount, "Should have exactly 3 apps with the duplicate name")
require.NotEmpty(t, app.ID)
require.NotEmpty(t, app.Name)
require.Equal(t, "http://localhost:3000", app.CallbackURL)
})
}
@ -1796,145 +1536,5 @@ func TestOAuth2RegistrationAccessToken(t *testing.T) {
})
}
// TestOAuth2ClientRegistrationValidation tests validation of client registration requests
func TestOAuth2ClientRegistrationValidation(t *testing.T) {
t.Parallel()
t.Run("ValidURIs", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
validURIs := []string{
"https://example.com/callback",
"http://localhost:8080/callback",
"custom-scheme://app/callback",
}
req := codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: validURIs,
ClientName: fmt.Sprintf("valid-uris-client-%d", time.Now().UnixNano()),
}
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
require.NoError(t, err)
require.Equal(t, validURIs, resp.RedirectURIs)
})
t.Run("InvalidURIs", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
uris []string
}{
{
name: "InvalidURL",
uris: []string{"not-a-url"},
},
{
name: "EmptyFragment",
uris: []string{"https://example.com/callback#"},
},
{
name: "Fragment",
uris: []string{"https://example.com/callback#fragment"},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Create new client for each sub-test to avoid shared state issues
subClient := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, subClient)
subCtx := testutil.Context(t, testutil.WaitLong)
req := codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: tc.uris,
ClientName: fmt.Sprintf("invalid-uri-client-%s-%d", tc.name, time.Now().UnixNano()),
}
_, err := subClient.PostOAuth2ClientRegistration(subCtx, req)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid_client_metadata")
})
}
})
t.Run("ValidGrantTypes", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
req := codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"https://example.com/callback"},
ClientName: fmt.Sprintf("valid-grant-types-client-%d", time.Now().UnixNano()),
GrantTypes: []string{"authorization_code", "refresh_token"},
}
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
require.NoError(t, err)
require.Equal(t, req.GrantTypes, resp.GrantTypes)
})
t.Run("InvalidGrantTypes", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
req := codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"https://example.com/callback"},
ClientName: fmt.Sprintf("invalid-grant-types-client-%d", time.Now().UnixNano()),
GrantTypes: []string{"unsupported_grant"},
}
_, err := client.PostOAuth2ClientRegistration(ctx, req)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid_client_metadata")
})
t.Run("ValidResponseTypes", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
req := codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"https://example.com/callback"},
ClientName: fmt.Sprintf("valid-response-types-client-%d", time.Now().UnixNano()),
ResponseTypes: []string{"code"},
}
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
require.NoError(t, err)
require.Equal(t, req.ResponseTypes, resp.ResponseTypes)
})
t.Run("InvalidResponseTypes", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
req := codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"https://example.com/callback"},
ClientName: fmt.Sprintf("invalid-response-types-client-%d", time.Now().UnixNano()),
ResponseTypes: []string{"token"}, // Not supported
}
_, err := client.PostOAuth2ClientRegistration(ctx, req)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid_client_metadata")
})
}
// NOTE: OAuth2 client registration validation tests have been migrated to
// oauth2provider/validation_test.go for better separation of concerns