feat(web): add /admin layout with role-gated load + sidebar
Hard route gate in admin/+layout.ts redirects non-admins to / before the layout (or any child) renders. AdminSidebar reads the active route from $app/state and applies a 12% accent-tinted bg + 2px forest-teal left strip on the active item. Overview landing shows pending-request count and Lidarr connected/unset, each linking to its sub-page. Shell nav exposes the Admin link only to is_admin users. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,18 @@ vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
}));
|
||||
|
||||
// Mutable handle so individual tests can flip the user between admin /
|
||||
// non-admin without re-importing the module.
|
||||
const userState = vi.hoisted(() => ({
|
||||
current: { id: '1', username: 'alice', is_admin: false } as {
|
||||
id: string;
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
} | null
|
||||
}));
|
||||
|
||||
vi.mock('$lib/auth/store.svelte', () => ({
|
||||
user: { value: { id: '1', username: 'alice', is_admin: false } },
|
||||
user: { get value() { return userState.current; } },
|
||||
logout: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
@@ -20,6 +30,7 @@ import { goto } from '$app/navigation';
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
userState.current = { id: '1', username: 'alice', is_admin: false };
|
||||
});
|
||||
|
||||
describe('Shell', () => {
|
||||
@@ -37,6 +48,29 @@ describe('Shell', () => {
|
||||
expect(screen.getByRole('link', { name: 'Playlists' })).toHaveAttribute('href', '/playlists');
|
||||
});
|
||||
|
||||
test('non-admin users do not see the Admin nav link', () => {
|
||||
userState.current = { id: '1', username: 'alice', is_admin: false };
|
||||
render(Shell);
|
||||
expect(screen.queryByRole('link', { name: 'Admin' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('admin users see the Admin nav link between Playlists and Settings', () => {
|
||||
userState.current = { id: '1', username: 'alice', is_admin: true };
|
||||
render(Shell);
|
||||
const link = screen.getByRole('link', { name: 'Admin' });
|
||||
expect(link).toHaveAttribute('href', '/admin');
|
||||
// Order check: Playlists then Admin then Settings.
|
||||
const labels = screen
|
||||
.getAllByRole('link')
|
||||
.map((el) => el.textContent?.trim())
|
||||
.filter(Boolean);
|
||||
const idxPlaylists = labels.indexOf('Playlists');
|
||||
const idxAdmin = labels.indexOf('Admin');
|
||||
const idxSettings = labels.indexOf('Settings');
|
||||
expect(idxPlaylists).toBeLessThan(idxAdmin);
|
||||
expect(idxAdmin).toBeLessThan(idxSettings);
|
||||
});
|
||||
|
||||
test('user-menu "Log out" calls logout() and navigates to /login', async () => {
|
||||
render(Shell);
|
||||
await fireEvent.click(screen.getByRole('button', { name: /alice/i }));
|
||||
|
||||
Reference in New Issue
Block a user