From 12d367e93c8f9727982ea9e1d2dcc30c28e43fef Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:33:55 +0200 Subject: [PATCH] Scoped PAT: Add unit tests --- .../Scoped/Form/ResourceAccessStep.tsx | 2 + .../Scoped/NewScopedTokenSheet.test.tsx | 249 ++++++++++++++++++ .../Scoped/NewScopedTokenSheet.tsx | 1 + 3 files changed, 252 insertions(+) create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.test.tsx diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccessStep.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccessStep.tsx index 5289f828dc1..65e10e43d4c 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccessStep.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccessStep.tsx @@ -171,6 +171,7 @@ export const ResourceAccessStep = ({ form, error }: ResourceAccessStepProps) => deletableBadge className="w-full h-[34px] min-h-auto" ref={field.ref} + id="organizationSlugs" /> @@ -208,6 +209,7 @@ export const ResourceAccessStep = ({ form, error }: ResourceAccessStepProps) => deletableBadge className="w-full h-[34px] min-h-auto" ref={field.ref} + id="projectRefs" /> diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.test.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.test.tsx new file mode 100644 index 00000000000..04180155caf --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.test.tsx @@ -0,0 +1,249 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { platformComponents as components } from 'api-types' +import { HttpResponse } from 'msw' +import { beforeEach, describe, expect, test } from 'vitest' + +import { NewScopedTokenSheet } from './NewScopedTokenSheet' +import type { ProfileContextType } from '@/lib/profile' +import { createMockOrganizationResponse, createMockProject } from '@/tests/helpers' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +type OrganizationResponse = components['schemas']['OrganizationResponse'] +type ProjectsResponse = components['schemas']['ListProjectsPaginatedResponse'] +type CreateTokenResponse = components['schemas']['CreateScopedAccessTokenResponse'] + +const user = userEvent.setup({ + writeToClipboard: true, +}) + +const PROFILE_CONTEXT: ProfileContextType = { + profile: { + id: 1, + auth0_id: 'auth0|test', + gotrue_id: 'gotrue-test', + username: 'testuser', + primary_email: 'test@example.com', + first_name: null, + last_name: null, + mobile: null, + is_alpha_user: false, + is_sso_user: false, + disabled_features: [], + free_project_limit: null, + }, + error: null, + isLoading: false, + isError: false, + isSuccess: true, +} + +const mockOrganizations = () => + addAPIMock({ + method: 'get', + path: '/platform/organizations', + response: () => + HttpResponse.json([ + createMockOrganizationResponse({ slug: 'acme-prod', name: 'Acme Production' }), + ]), + }) + +const mockProjects = () => + addAPIMock({ + method: 'get', + path: '/platform/projects', + response: () => + HttpResponse.json({ + pagination: { count: 1, limit: 100, offset: 0 }, + projects: [ + { + ...createMockProject({ + id: 1, + ref: 'project-1', + name: 'Project 1', + organization_id: 1, + }), + organization_slug: 'acme-prod', + preview_branch_refs: [], + }, + ], + }), + }) + +const mockCreateToken = () => + addAPIMock({ + method: 'post', + path: '/platform/profile/scoped-access-tokens', + response: () => + HttpResponse.json({ + created_at: '', + expires_at: null, + id: 'plop', + last_used_at: null, + name: 'test', + token: 'a_token_value', + token_alias: '', + permissions: [], + }), + }) + +describe('NewScopedTokenSheet', () => { + beforeEach(() => { + mockOrganizations() + mockProjects() + mockCreateToken() + }) + test('requires a token name', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('Please enter a name for the token')) + }) + // Project scope tests + test('requires an organization when scope is Project', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('Please select an organization to continue.')) + }) + test('requires a project when scope is Project', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + fireEvent.click(await screen.findByRole('combobox', { name: 'Organization' })) + fireEvent.click(await screen.findByRole('option', { name: 'Acme Production' })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('Please select a project to continue.')) + }) + test('requires permissions when scope is Project', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + fireEvent.click(await screen.findByRole('combobox', { name: 'Organization' })) + fireEvent.click(await screen.findByRole('option', { name: 'Acme Production' })) + fireEvent.click(await screen.findByRole('combobox', { name: 'Projects' })) + fireEvent.click(await screen.findByRole('option', { name: 'Project 1' })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('No permissions selected', { selector: '[role="alert"] *' })) + }) + test('creates the token when scope is Project', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + fireEvent.click(await screen.findByRole('combobox', { name: 'Organization' })) + fireEvent.click(await screen.findByRole('option', { name: 'Acme Production' })) + fireEvent.click(await screen.findByRole('combobox', { name: 'Projects' })) + fireEvent.click(await screen.findByRole('option', { name: 'Project 1' })) + fireEvent.click(await screen.findByLabelText('Project Settings', { exact: false })) + fireEvent.click(await screen.findByRole('option', { name: 'Read' })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + // Review screen + await screen.findByText('Low — single-project read-only access') + fireEvent.click(await screen.findByRole('button', { name: 'Create token' })) + // If we can click this checkbox, the token was created + fireEvent.click(await screen.findByRole('button', { name: 'Copy' })) + await waitFor(async () => + expect(await window.navigator.clipboard.readText()).toEqual('a_token_value') + ) + fireEvent.click(await screen.findByLabelText('I have copied the key and stored it securely')) + fireEvent.click(await screen.findByRole('button', { name: 'Done' })) + // Dialog has been closed + await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) + }) + + // Organization scope tests + test('requires an organization when scope is Organization', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + await user.click(await screen.findByRole('radio', { name: /Organization/ })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('Please select an organization to continue.')) + }) + test('requires permissions when scope is Organization', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + await user.click(await screen.findByRole('radio', { name: /Organization/ })) + fireEvent.click(await screen.findByRole('combobox', { name: 'Organizations' })) + fireEvent.click(await screen.findByRole('option', { name: 'Acme Production' })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('No permissions selected', { selector: '[role="alert"] *' })) + }) + test('creates the token when scope is Organization', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + await user.click(await screen.findByRole('radio', { name: /Organization/ })) + fireEvent.click(await screen.findByRole('combobox', { name: 'Organizations' })) + fireEvent.click(await screen.findByRole('option', { name: 'Acme Production' })) + fireEvent.click(await screen.findByLabelText('Project Settings', { exact: false })) + fireEvent.click(await screen.findByRole('option', { name: 'Read' })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + // Review screen + await screen.findByText('Low — organization-wide read-only access') + fireEvent.click(await screen.findByRole('button', { name: 'Create token' })) + // If we can click this checkbox, the token was created + fireEvent.click(await screen.findByRole('button', { name: 'Copy' })) + await waitFor(async () => + expect(await window.navigator.clipboard.readText()).toEqual('a_token_value') + ) + fireEvent.click(await screen.findByLabelText('I have copied the key and stored it securely')) + fireEvent.click(await screen.findByRole('button', { name: 'Done' })) + // Dialog has been closed + await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) + }) + + // Account scope tests + test('requires permissions when scope is Account', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + await user.click(await screen.findByText('Advanced options')) + await user.click( + await screen.findByText( + 'I understand this token is not limited to one project or organization.' + ) + ) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + expect(await screen.findByText('No permissions selected', { selector: '[role="alert"] *' })) + }) + test('creates the token when scope is Account', async () => { + customRender(, { profileContext: PROFILE_CONTEXT }) + fireEvent.click(await screen.findByRole('button', { name: 'Generate new token' })) + await screen.findByRole('dialog') + await user.type(await screen.findByLabelText('Name'), 'test') + await user.click(await screen.findByText('Advanced options')) + await user.click( + await screen.findByText( + 'I understand this token is not limited to one project or organization.' + ) + ) + fireEvent.click(await screen.findByLabelText('Project Settings', { exact: false })) + fireEvent.click(await screen.findByRole('option', { name: 'Read' })) + fireEvent.click(await screen.findByRole('button', { name: 'Review access' })) + // Review screen + await screen.findByText('Elevated — account-wide read-only access') + fireEvent.click(await screen.findByRole('button', { name: 'Create token' })) + // If we can click this checkbox, the token was created + fireEvent.click(await screen.findByRole('button', { name: 'Copy' })) + await waitFor(async () => + expect(await window.navigator.clipboard.readText()).toEqual('a_token_value') + ) + fireEvent.click(await screen.findByLabelText('I have copied the key and stored it securely')) + fireEvent.click(await screen.findByRole('button', { name: 'Done' })) + // Dialog has been closed + await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) + }) +}) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx index 14b2f72985c..cd6ef5310e0 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx @@ -66,6 +66,7 @@ export const NewScopedTokenSheet = () => { setIsOpen(open) } + console.log({ createdToken }) return (