Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion ui/e2e/live/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ import { request, expect, type APIRequestContext } from '@playwright/test';
* admin: the better-auth cookie for the API origin and the Bearer token the
* console adapter reads from the APP origin's localStorage. It is returned,
* never written to disk, so no path has to be rooted.
*
* ⛔ Every request context this module builds names its `storageState`. Inside
* `@playwright/test`, a request context created without one inherits the
* config's `use.storageState`, which is the global setup's ADMIN session
* cookie (measured: `playwright.request.newContext()` and the imported
* `request.newContext()` both start with that cookie). A member's Bearer sent
* from such a context is a request signed in as two accounts, which the server
* refuses since objectstack#31469 (`CREDENTIALS_NAME_DIFFERENT_ACCOUNTS`; a data
* route answers its own 401). So the sign-in starts from an empty state, and a
* spec acting as the member over the API uses {@link memberRequestContext}.
*/
const APP = process.env.LIVE_APP_URL || 'http://localhost:5180';
const API = process.env.LIVE_API_URL || 'http://localhost:3000';
Expand Down Expand Up @@ -68,7 +78,8 @@ export async function signInMember(admin: APIRequestContext, permissionSet: stri
).toBe('USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL');
}

const ctx = await request.newContext();
// An empty state, not the inherited admin cookie: the sign-in names the member only.
const ctx = await request.newContext({ storageState: { cookies: [], origins: [] } });
try {
const res = await ctx.post(`${API}/api/v1/auth/sign-in/email`, {
data: { email, password: MEMBER_PASSWORD },
Expand All @@ -90,3 +101,16 @@ export async function signInMember(admin: APIRequestContext, permissionSet: stri
await ctx.dispose();
}
}

/**
* A request context that acts as `member` and as no one else: the member's own
* session cookie and its Bearer token (objectstack#31827). Its `storageState`
* is named so the config's admin cookie is not inherited (see the module
* comment). The caller disposes it.
*/
export async function memberRequestContext(member: LiveMember): Promise<APIRequestContext> {
return request.newContext({
storageState: { cookies: member.storageState.cookies, origins: [] },
extraHTTPHeaders: { Authorization: `Bearer ${member.token}` },
});
}
5 changes: 2 additions & 3 deletions ui/e2e/live/read-only-member-record.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect, type Page } from '@playwright/test';
import { signInMember } from './member';
import { memberRequestContext, signInMember } from './member';
import { writesRecordsOf } from './data-writes';

/**
Expand Down Expand Up @@ -49,7 +49,6 @@ test('a member who may read but not write an inquiry gets a read-only form and n
browser,
page,
request,
playwright,
}) => {
// Three page loads in two browser contexts, one of them cold.
test.setTimeout(120_000);
Expand All @@ -66,7 +65,7 @@ test('a member who may read but not write an inquiry gets a read-only form and n
const member = await signInMember(request, 'showcase_manager');

await test.step('premise: the server lets the member read the record and refuses its update', async () => {
const api = await playwright.request.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${member.token}` } });
const api = await memberRequestContext(member);
try {
const read = await api.get(`${API}/api/v1/data/${OBJECT}/${id}`);
expect(read.status(), 'the member reads the admin-owned inquiry').toBe(200);
Expand Down
Loading