test(e2e): role-shadowing specs in apps + scripts suites
Lifts loginAsUserToken + pageWithUserToken out of members.spec.ts into fixtures/role-page.ts (third file that needs them). Adds shadowing coverage: viewer member sees no New-app / Add-domain / Settings / Save / +Add-route, editor sees Save but no Delete header, and CodeMirror renders contenteditable=false for viewers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,36 @@ import { expect, type Page } from '@playwright/test';
|
||||
import { test } from '../fixtures/ids';
|
||||
import { CleanupRegistry } from '../fixtures/cleanup';
|
||||
import { adminApi } from '../fixtures/api';
|
||||
import { loginAsUserToken, pageWithUserToken } from '../fixtures/role-page';
|
||||
|
||||
const MEMBER_PW = 'e2e-member-pw';
|
||||
|
||||
async function seedAppAndMember(opts: {
|
||||
slug: string;
|
||||
username: string;
|
||||
role: 'viewer' | 'editor' | 'app_admin';
|
||||
}): Promise<{ appId: string; userId: string }> {
|
||||
const api = await adminApi();
|
||||
try {
|
||||
const appRes = await api.post('/api/v1/admin/apps', {
|
||||
data: { slug: opts.slug, name: opts.slug }
|
||||
});
|
||||
expect(appRes.ok()).toBe(true);
|
||||
const appId = ((await appRes.json()) as { id: string }).id;
|
||||
const userRes = await api.post('/api/v1/admin/admins', {
|
||||
data: { username: opts.username, password: MEMBER_PW, instance_role: 'member' }
|
||||
});
|
||||
expect(userRes.ok()).toBe(true);
|
||||
const userId = ((await userRes.json()) as { id: string }).id;
|
||||
const memberRes = await api.post(`/api/v1/admin/apps/${opts.slug}/members`, {
|
||||
data: { user_id: userId, role: opts.role }
|
||||
});
|
||||
expect(memberRes.ok()).toBe(true);
|
||||
return { appId, userId };
|
||||
} finally {
|
||||
await api.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Phase B2 — Apps Lifecycle. Create, view, edit, delete, plus the
|
||||
// historical-slug takeover flow and adversarial inputs.
|
||||
@@ -224,3 +254,82 @@ test.describe('B2 apps adversarial', () => {
|
||||
await expect(page.getByRole('button', { name: 'Settings' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('B2 apps role shadowing', () => {
|
||||
test('viewer member sees no "New app" on the apps list', async ({
|
||||
browser,
|
||||
uniqueSlug,
|
||||
uniqueUsername
|
||||
}) => {
|
||||
const slug = uniqueSlug('vlist');
|
||||
const username = uniqueUsername('viewer');
|
||||
const { userId } = await seedAppAndMember({ slug, username, role: 'viewer' });
|
||||
cleanup.app(slug);
|
||||
cleanup.adminUser(userId);
|
||||
|
||||
const token = await loginAsUserToken(username, MEMBER_PW);
|
||||
const page = await pageWithUserToken(browser, token);
|
||||
try {
|
||||
await page.goto('/admin/apps');
|
||||
// Member can see the apps list (just the one they belong to)
|
||||
// but the create-app affordance is hidden.
|
||||
await expect(page.getByRole('link', { name: new RegExp(slug) })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /^New app$/ })).toHaveCount(0);
|
||||
} finally {
|
||||
await page.context().close();
|
||||
}
|
||||
});
|
||||
|
||||
test('viewer sees no Add domain form and no Settings tab on app detail', async ({
|
||||
browser,
|
||||
uniqueSlug,
|
||||
uniqueUsername
|
||||
}) => {
|
||||
const slug = uniqueSlug('vdom');
|
||||
const username = uniqueUsername('viewer');
|
||||
const { userId } = await seedAppAndMember({ slug, username, role: 'viewer' });
|
||||
cleanup.app(slug);
|
||||
cleanup.adminUser(userId);
|
||||
|
||||
const token = await loginAsUserToken(username, MEMBER_PW);
|
||||
const page = await pageWithUserToken(browser, token);
|
||||
try {
|
||||
await page.goto(`/admin/apps/${slug}`);
|
||||
await expect(
|
||||
page.getByRole('button', { name: /^Scripts \(\d+\)$/ })
|
||||
).toBeVisible();
|
||||
// Settings tab is absent.
|
||||
await expect(page.getByRole('button', { name: /^Settings$/ })).toHaveCount(0);
|
||||
// Domains tab still listable, but no Add-domain submit.
|
||||
await page.getByRole('button', { name: /^Domains \(\d+\)$/ }).click();
|
||||
await expect(page.getByRole('button', { name: /^Add domain$/ })).toHaveCount(0);
|
||||
} finally {
|
||||
await page.context().close();
|
||||
}
|
||||
});
|
||||
|
||||
test('editor sees New script but no Settings tab', async ({
|
||||
browser,
|
||||
uniqueSlug,
|
||||
uniqueUsername
|
||||
}) => {
|
||||
const slug = uniqueSlug('edit');
|
||||
const username = uniqueUsername('editor');
|
||||
const { userId } = await seedAppAndMember({ slug, username, role: 'editor' });
|
||||
cleanup.app(slug);
|
||||
cleanup.adminUser(userId);
|
||||
|
||||
const token = await loginAsUserToken(username, MEMBER_PW);
|
||||
const page = await pageWithUserToken(browser, token);
|
||||
try {
|
||||
await page.goto(`/admin/apps/${slug}`);
|
||||
await expect(page.getByRole('button', { name: /^New script$/ })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /^Settings$/ })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole('button', { name: /^Members \(\d+\)$/ })
|
||||
).toHaveCount(0);
|
||||
} finally {
|
||||
await page.context().close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user