diff --git a/CLAUDE.md b/CLAUDE.md index 88811fd..2cd99cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,27 @@ bun build # Production build bun lint # Run ESLint ``` +## Testing + +### Unit Tests +```bash +bun test # Run unit tests +``` + +### E2E Tests +```bash +bunx playwright test # Run all E2E tests +bunx playwright test --ui # Interactive UI mode +bunx playwright test e2e/admin # Run admin tests only +bunx playwright test --debug # Debug mode +``` + +### Testing Patterns +- E2E tests are in `e2e/` directory using Playwright +- Use `data-testid` attributes for reliable element selection +- Test helpers are in `e2e/fixtures/test-helpers.ts` +- Tests verify DB state by navigating to edit pages after creation + ## Architecture This is a Next.js 16 authentication app using the App Router with React 19. diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..faac630 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/components/admin/client-form.tsx b/components/admin/client-form.tsx index 0713b24..25f327e 100644 --- a/components/admin/client-form.tsx +++ b/components/admin/client-form.tsx @@ -10,7 +10,12 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { createOAuthClient } from "@/actions/admin/clients/create"; import { updateOAuthClient } from "@/actions/admin/clients/update"; -import { SUPPORTED_SCOPES } from "@/lib/validations/oauth"; +import { + SCOPE_RESOURCES, + SPECIAL_SCOPES, + OPERATIONS, + getScopesByResource, +} from "@/lib/scopes"; interface ClientFormProps { client?: { @@ -67,6 +72,34 @@ export function ClientForm({ client }: ClientFormProps) { } }; + const handleResourceToggle = (resourceKey: string) => { + const resourceScopes = getScopesByResource(resourceKey); + const scopeKeys = resourceScopes.map((s) => s.key); + const allSelected = scopeKeys.every((key) => allowedScopes.includes(key)); + + if (allSelected) { + // Deselect all scopes for this resource + setAllowedScopes(allowedScopes.filter((s) => !scopeKeys.includes(s))); + } else { + // Select all scopes for this resource + const newScopes = new Set([...allowedScopes, ...scopeKeys]); + setAllowedScopes(Array.from(newScopes)); + } + }; + + const isResourceFullySelected = (resourceKey: string) => { + const resourceScopes = getScopesByResource(resourceKey); + return resourceScopes.every((s) => allowedScopes.includes(s.key)); + }; + + const isResourcePartiallySelected = (resourceKey: string) => { + const resourceScopes = getScopesByResource(resourceKey); + const selectedCount = resourceScopes.filter((s) => + allowedScopes.includes(s.key) + ).length; + return selectedCount > 0 && selectedCount < resourceScopes.length; + }; + const handleCopy = (text: string, type: string) => { navigator.clipboard.writeText(text); setCopied(type); @@ -137,7 +170,7 @@ export function ClientForm({ client }: ClientFormProps) {
- + @@ -200,6 +234,7 @@ export function ClientForm({ client }: ClientFormProps) { setName(e.target.value)} @@ -212,6 +247,7 @@ export function ClientForm({ client }: ClientFormProps) {