mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-09-21 09:45:44 +00:00
test(webui): cover account page OAuth gating and the name-change form
- settings-account: local accounts expose name/email/password; OAuth accounts hide the password card and email editing, keep the name editable, and label the provider (known label, raw fallback, then generic); renders nothing without a user - name-change-form (previously untested): seeds the current name, submits the trimmed value and refreshes auth, blocks an empty name, maps Users.NotFound to friendly copy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9a5a8b53a5
commit
96f346e6ec
@@ -0,0 +1,77 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { put, refreshAuthInfo } = vi.hoisted(() => ({
|
||||
put: vi.fn(),
|
||||
refreshAuthInfo: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// Keep the pure helpers (`getApiErrorCode`, `getApiErrorMessage`) real — only the network call is stubbed.
|
||||
vi.mock('@/lib/axios', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/axios')>();
|
||||
|
||||
return { ...actual, api: { ...actual.api, put } };
|
||||
});
|
||||
|
||||
vi.mock('@/providers/user-provider', () => ({
|
||||
useUser: () => ({ authInfo: { user: { name: 'Old Name' } }, refreshAuthInfo }),
|
||||
}));
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
|
||||
|
||||
import { NameChangeForm } from './name-change-form';
|
||||
|
||||
const apiError = (code: string, msg: string) => ({ response: { data: { code, msg, status: 'error' } } });
|
||||
|
||||
beforeEach(() => {
|
||||
put.mockReset().mockResolvedValue({ status: 'success' });
|
||||
refreshAuthInfo.mockClear();
|
||||
});
|
||||
|
||||
describe('NameChangeForm', () => {
|
||||
it('seeds the field with the current name', () => {
|
||||
render(<NameChangeForm />);
|
||||
|
||||
expect((screen.getByLabelText('Display name') as HTMLInputElement).value).toBe('Old Name');
|
||||
});
|
||||
|
||||
it('submits the trimmed name and refreshes auth before closing', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSuccess = vi.fn();
|
||||
render(<NameChangeForm onSuccess={onSuccess} />);
|
||||
|
||||
const input = screen.getByLabelText('Display name');
|
||||
await user.clear(input);
|
||||
await user.type(input, ' New Name ');
|
||||
await user.click(screen.getByRole('button', { name: 'Update Name' }));
|
||||
|
||||
await waitFor(() => expect(onSuccess).toHaveBeenCalledOnce());
|
||||
expect(put).toHaveBeenCalledWith('/user/name', { name: 'New Name' });
|
||||
expect(refreshAuthInfo).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('blocks an empty name without calling the API', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<NameChangeForm />);
|
||||
|
||||
await user.clear(screen.getByLabelText('Display name'));
|
||||
await user.click(screen.getByRole('button', { name: 'Update Name' }));
|
||||
|
||||
expect(await screen.findByText('Name is required')).toBeInTheDocument();
|
||||
expect(put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('maps the user-not-found code to friendly copy', async () => {
|
||||
const user = userEvent.setup();
|
||||
put.mockRejectedValueOnce(apiError('Users.NotFound', 'user not found'));
|
||||
render(<NameChangeForm />);
|
||||
|
||||
const input = screen.getByLabelText('Display name');
|
||||
await user.clear(input);
|
||||
await user.type(input, 'Whoever');
|
||||
await user.click(screen.getByRole('button', { name: 'Update Name' }));
|
||||
|
||||
expect(await screen.findByText('User not found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { authState } = vi.hoisted(() => ({ authState: { value: null as unknown } }));
|
||||
|
||||
vi.mock('@/providers/user-provider', () => ({
|
||||
useUser: () => ({ authInfo: authState.value, refreshAuthInfo: vi.fn() }),
|
||||
}));
|
||||
|
||||
import SettingsAccount from './settings-account';
|
||||
|
||||
const localUser = { created_at: '2026-01-15T00:00:00Z', mail: 'local@example.com', name: 'Local User', type: 'local' };
|
||||
const githubUser = { mail: 'gh@example.com', name: 'GH User', provider: 'github', type: 'oauth' };
|
||||
|
||||
beforeEach(() => {
|
||||
authState.value = null;
|
||||
});
|
||||
|
||||
describe('SettingsAccount gating', () => {
|
||||
it('renders nothing without a user', () => {
|
||||
const { container } = render(<SettingsAccount />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('exposes name, email and password for a local account', () => {
|
||||
authState.value = { user: localUser };
|
||||
render(<SettingsAccount />);
|
||||
|
||||
expect(screen.getByText('Local account')).toBeInTheDocument();
|
||||
expect(screen.getByText('Password')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Change' })).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('hides password and email editing for an OAuth account but keeps the name editable', () => {
|
||||
authState.value = { user: githubUser };
|
||||
render(<SettingsAccount />);
|
||||
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
expect(screen.getByText('Linked from your GitHub.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Password')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Change' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('labels an unknown provider by its raw name, then a generic fallback', () => {
|
||||
authState.value = { user: { mail: 'x@e.com', name: 'X', provider: 'gitlab', type: 'oauth' } };
|
||||
const { unmount } = render(<SettingsAccount />);
|
||||
expect(screen.getByText('gitlab')).toBeInTheDocument();
|
||||
unmount();
|
||||
|
||||
authState.value = { user: { mail: 'y@e.com', name: 'Y', type: 'oauth' } };
|
||||
render(<SettingsAccount />);
|
||||
expect(screen.getByText('OAuth account')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the name form on Change for an OAuth user', async () => {
|
||||
const user = userEvent.setup();
|
||||
authState.value = { user: githubUser };
|
||||
render(<SettingsAccount />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Change' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Update Name' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user