Auth logs in via a log in link and logs out from the settings page Renders/ /notifications /settings
Log in link page Side nav Settings page Side nav Error page Log in link page Arrange
Create log in link user N-ZAP → logInLink
const logInLink = await factories.LogInLinkFactory .create ({
userId: NZAP_TEST_ID ,
});Log in link page Act
Open the log in link page logInLink.code
await logInLinkPage.goto (logInLink.code);Assert
the page URL is “/”
await expect (page).toHaveURL ("/" );Side nav Assert
footer username reads “N-ZAP”
await expect (sideNav.locators.footerUsername).toHaveText ("N-ZAP" );log in button is not visible
await isNotVisible (sideNav.locators.logInButton);Settings page Act
Open the settings page
await settings.goto ();Log out
await settings.logOut ();Side nav Assert
log in button is visible
await expect (sideNav.locators.logInButton).toBeVisible ();footer username is not visible
await isNotVisible (sideNav.locators.footerUsername);Act
Go to NOTIFICATIONS_URL
await navigate ({ page, url: NOTIFICATIONS_URL });Error page Assert
heading “Authentication required” is visible
await expect (errorPage.heading ("Authentication required" )).toBeVisible ();Log in link page Act
the first log in consumed the single use link
Fetch response logInLink.code → reusedLink
const reusedLink = await logInLinkPage.fetchResponse (logInLink.code);Assert
reusedLink.status is 400
expect (reusedLink.status).toBe (400 );Source test ("logs in via a log in link and logs out from the settings page" , async ({
page,
factories,
}) => {
const logInLink = await factories.LogInLinkFactory .create ({
userId: NZAP_TEST_ID ,
});
const logInLinkPage = new LogInLinkPage (page);
const sideNav = new SideNav (page);
await logInLinkPage.goto (logInLink.code);
await expect (page).toHaveURL ("/" );
await expect (sideNav.locators.footerUsername).toHaveText ("N-ZAP" );
await isNotVisible (sideNav.locators.logInButton);
const settings = new SettingsPage (page);
await settings.goto ();
await settings.logOut ();
await expect (sideNav.locators.logInButton).toBeVisible ();
await isNotVisible (sideNav.locators.footerUsername);
const errorPage = new ErrorPage (page);
await navigate ({ page, url: NOTIFICATIONS_URL });
await expect (errorPage.heading ("Authentication required" )).toBeVisible ();
// the first log in consumed the single use link
const reusedLink = await logInLinkPage.fetchResponse (logInLink.code);
expect (reusedLink.status).toBe (400 );
})a session of an account deleted by a migration falls back to logged out Renders/ /notifications
Log in link page Side nav Admin actions page Side nav Error page Log in link page Side nav Arrange
Create user KEEPER → keeper
const keeper = await factories.UserFactory .create (KEEPER );Create user LEAVER → leaver
const leaver = await factories.UserFactory .create (LEAVER );Create log in link user id leaver.id → leaverLogInLink
const leaverLogInLink = await factories.LogInLinkFactory .create ({
userId: leaver.id,
});Create log in link user id keeper.id → keeperLogInLink
const keeperLogInLink = await factories.LogInLinkFactory .create ({
userId: keeper.id,
});Log in link page Act
Open the log in link page leaverLogInLink.code
await logInLinkPage.goto (leaverLogInLink.code);Side nav Assert
footer username reads LEAVER .discordName
await expect (sideNav.locators.footerUsername).toHaveText (
LEAVER .discordName,
);Act
Open second user browser, workerBaseURL → admin
const admin = await openSecondUser (browser, workerBaseURL);Admin actions page Act
Open the admin actions page
await adminActions.goto ();Migrate user old user name KEEPER .discordName new user name LEAVER .discordName
await adminActions.migrateUser ({
oldUserName: KEEPER .discordName,
newUserName: LEAVER .discordName,
});Close
await admin.close ();Arrange
sessionCookie = async () => (await page.context ().cookies ()).find ( (cookie) => cookie.name ==…
const sessionCookie = async () =>
(await page.context ().cookies ()).find (
(cookie) => cookie.name === SESSION_COOKIE_NAME ,
);Act
Session cookie → staleCookie
const staleCookie = await sessionCookie ();Assert
staleCookie be defined
expect (staleCookie).toBeDefined ();Act
Go to “/”
await navigate ({ page, url: "/" });Assert
no error page
await expectNoErrorPage (page);Side nav Assert
log in button is visible
await expect (sideNav.locators.logInButton).toBeVisible ();footer username is not visible
await isNotVisible (sideNav.locators.footerUsername);sessionCookie () be undefined
expect (await sessionCookie ()).toBeUndefined ();Act
Go to NOTIFICATIONS_URL
await navigate ({ page, url: NOTIFICATIONS_URL });Error page Assert
heading “Authentication required” is visible
await expect (errorPage.heading ("Authentication required" )).toBeVisible ();Act
Add cookies staleCookie!
await page.context ().addCookies ([staleCookie!]);Log in link page Act
Open the log in link page keeperLogInLink.code
await logInLinkPage.goto (keeperLogInLink.code);Side nav Assert
footer username reads KEEPER .discordName
await expect (sideNav.locators.footerUsername).toHaveText (
KEEPER .discordName,
);Source test ("a session of an account deleted by a migration falls back to logged out" , async ({
page,
browser,
workerBaseURL,
factories,
}) => {
const keeper = await factories.UserFactory .create (KEEPER );
const leaver = await factories.UserFactory .create (LEAVER );
const leaverLogInLink = await factories.LogInLinkFactory .create ({
userId: leaver.id,
});
const keeperLogInLink = await factories.LogInLinkFactory .create ({
userId: keeper.id,
});
const logInLinkPage = new LogInLinkPage (page);
const sideNav = new SideNav (page);
await logInLinkPage.goto (leaverLogInLink.code);
await expect (sideNav.locators.footerUsername).toHaveText (
LEAVER .discordName,
);
const admin = await openSecondUser (browser, workerBaseURL);
const adminActions = new AdminActionsPage (admin.page);
await adminActions.goto ();
await adminActions.migrateUser ({
oldUserName: KEEPER .discordName,
newUserName: LEAVER .discordName,
});
await admin.close ();
const sessionCookie = async () =>
(await page.context ().cookies ()).find (
(cookie) => cookie.name === SESSION_COOKIE_NAME ,
);
const staleCookie = await sessionCookie ();
expect (staleCookie).toBeDefined ();
await navigate ({ page, url: "/" });
await expectNoErrorPage (page);
await expect (sideNav.locators.logInButton).toBeVisible ();
await isNotVisible (sideNav.locators.footerUsername);
expect (await sessionCookie ()).toBeUndefined ();
const errorPage = new ErrorPage (page);
await navigate ({ page, url: NOTIFICATIONS_URL });
await expect (errorPage.heading ("Authentication required" )).toBeVisible ();
await page.context ().addCookies ([staleCookie!]);
await logInLinkPage.goto (keeperLogInLink.code);
await expect (sideNav.locators.footerUsername).toHaveText (
KEEPER .discordName,
);
})Renders/ /auth
Discord authorize interceptor Front page Side nav Discord authorize interceptor Discord authorize interceptor Act
Install context
await discord.install (context);Front page Act
Open the front page
await frontPage.goto ();Side nav Act
Click log in button
await sideNav.locators.logInButton.click ();Discord authorize interceptor Act
Wait for capture
await discord.waitForCapture ();Assert
href match /discord\.com\/(api\/)?oauth2\/authorize/
expect (discord.authorizeUrl.href).toMatch (
/discord\.com\/(api\/)?oauth2\/authorize/,
);param “client_id” is “123”
expect (discord.param ("client_id" )).toBe ("123" );param “response_type” is “code”
expect (discord.param ("response_type" )).toBe ("code" );param “scope” contains “identify”
expect (discord.param ("scope" )).toContain ("identify" );param “state” is truthy
expect (discord.param ("state" )).toBeTruthy ();param “redirect_uri” match /^http:\/\/localhost:\d+\/auth\/callback$/
expect (discord.param ("redirect_uri" )).toMatch (
/^http:\/\/localhost:\d+\/auth\/callback$/,
);Source test ("log in button starts the Discord OAuth flow" , async ({
page,
context,
}) => {
const discord = new DiscordAuthorizeInterceptor ();
await discord.install (context);
const frontPage = new FrontPage (page);
await frontPage.goto ();
const sideNav = new SideNav (page);
await sideNav.locators.logInButton.click ();
await discord.waitForCapture ();
expect (discord.authorizeUrl.href).toMatch (
/discord\.com\/(api\/)?oauth2\/authorize/,
);
expect (discord.param ("client_id" )).toBe ("123" );
expect (discord.param ("response_type" )).toBe ("code" );
expect (discord.param ("scope" )).toContain ("identify" );
expect (discord.param ("state" )).toBeTruthy ();
expect (discord.param ("redirect_uri" )).toMatch (
/^http:\/\/localhost:\d+\/auth\/callback$/,
);
})logged-in responses are never publicly cacheable Act
Get “/cache-deception.css” → staticLookingNotFound
const staticLookingNotFound = await page.request.get (
"/cache-deception.css" ,
);Assert
staticLookingNotFound.status () is 404
expect (staticLookingNotFound.status ()).toBe (404 );staticLookingNotFound.headers ()["cache-control" ] is “private, no-cache”
expect (staticLookingNotFound.headers ()["cache-control" ]).toBe (
"private, no-cache" ,
);Act
Get “/settings.data” → loaderData
const loaderData = await page.request.get ("/settings.data" );Assert
loaderData.ok () is true
expect (loaderData.ok ()).toBe (true );loaderData.headers ()["cache-control" ] is “private, no-cache”
expect (loaderData.headers ()["cache-control" ]).toBe ("private, no-cache" );Source test ("logged-in responses are never publicly cacheable" , async ({ page }) => {
await impersonate (page);
const staticLookingNotFound = await page.request.get (
"/cache-deception.css" ,
);
expect (staticLookingNotFound.status ()).toBe (404 );
expect (staticLookingNotFound.headers ()["cache-control" ]).toBe (
"private, no-cache" ,
);
const loaderData = await page.request.get ("/settings.data" );
expect (loaderData.ok ()).toBe (true );
expect (loaderData.headers ()["cache-control" ]).toBe ("private, no-cache" );
})LogInLinkPage.gotoe2e/pages/auth/log-in-link-page.ts:12
No JSDoc
async goto (code: string ) {
await navigate ({ page: this .page, url: `/auth/login?code=${code}` });
}SideNav.locators.footerUsernamee2e/pages/layout/side-nav.ts:23
No JSDoc
this .root.locator ("a[class*='sideNavFooterUser']" )SideNav.locators.logInButtone2e/pages/layout/side-nav.ts:20
No JSDoc
this .root.getByRole ("button" , {
name: "Log in via Discord" ,
})isNotVisiblee2e/helpers/playwright.ts:745
No JSDoc
export function isNotVisible (locator: Locator ) {
return expect (locator).toHaveCount (0 );
}SettingsPage.gotoe2e/pages/settings/settings-page.ts:52
No JSDoc
async goto (tab?: SettingsTab ) {
await navigate ({
page: this .page,
url: tab ? `${SETTINGS_PAGE}?tab=${tab}` : SETTINGS_PAGE ,
});
}SettingsPage.logOute2e/pages/settings/settings-page.ts:60
async logOut () {
await this .locators.logOutButton.click ();
await this .page.waitForURL ("/" );
await expectIsHydrated (this .page);
}ErrorPage.headinge2e/pages/layout/error-page.ts:28
No JSDoc
heading (name: string | RegExp ) {
return this .locators.root.getByRole ("heading" , { name });
}LogInLinkPage.fetchResponsee2e/pages/auth/log-in-link-page.ts:17
async fetchResponse (code: string ) {
const response = await this .page.request.get (`/auth/login?code=${code}` );
return { status: response.status () };
}LogInLinkPagee2e/pages/auth/log-in-link-page.ts:5
SideNave2e/pages/layout/side-nav.ts:11
SettingsPagee2e/pages/settings/settings-page.ts:18
ErrorPagee2e/pages/layout/error-page.ts:5
openSecondUsere2e/helpers/chat.ts:7
export async function openSecondUser (
browser: Browser ,
baseURL: string ,
userId = ADMIN_ID ,
) {
const context = await browser.newContext ({ baseURL });
// the `context` fixture blocks these for the test's own context only
await context.route (
/^https:\/\/fonts\.(googleapis|gstatic)\.com\//,
(route) => route.abort (),
);
const page = await context.newPage ();
await impersonate (page, userId);
// … AdminActionsPage.gotoe2e/pages/admin/admin-actions-page.ts:20
No JSDoc
async goto () {
await navigate ({ page: this .page, url: ADMIN_PAGE });
}AdminActionsPage.migrateUsere2e/pages/admin/admin-actions-page.ts:77
No JSDoc
async migrateUser ({
oldUserName,
newUserName,
}: {
oldUserName: string ;
newUserName: string ;
}) {
const form = this .form ("Migrate user data" );
await selectUser ({
page: this .page,
userName: oldUserName,
labelName: "Old user" ,
within: form,
});
// … expectNoErrorPagee2e/helpers/playwright.ts:741
export async function expectNoErrorPage (page: Page ) {
await expect (page.getByTestId ("error-page" )).toHaveCount (0 );
}AdminActionsPagee2e/pages/admin/admin-actions-page.ts:13
DiscordAuthorizeInterceptor.installe2e/pages/auth/discord-authorize.ts:13
No JSDoc
async install (context: BrowserContext ) {
await context.route ("**/auth" , async (route) => {
if (route.request ().method () !== "POST" ) return route.fallback ();
const response = await route.fetch ({ maxRedirects: 0 });
const location = response.headers ().location;
if (!location?.includes (AUTHORIZE_PATHNAME )) {
return route.fulfill ({ response });
}
this .capturedUrl = new URL (location);
return route.fulfill ({
contentType: "text/html" ,
body: "<h1>Discord authorize stub</h1>" ,
// … FrontPage.gotoe2e/pages/front-page/front-page.ts:19
No JSDoc
async goto () {
await navigate ({ page: this .page, url: "/" });
}DiscordAuthorizeInterceptor.waitForCapturee2e/pages/auth/discord-authorize.ts:32
No JSDoc
async waitForCapture () {
await expect.poll (() => this .capturedUrl).not.toBeNull ();
}DiscordAuthorizeInterceptor.parame2e/pages/auth/discord-authorize.ts:43
No JSDoc
param (name: string ) {
return this .authorizeUrl.searchParams.get (name);
}DiscordAuthorizeInterceptore2e/pages/auth/discord-authorize.ts:10
FrontPagee2e/pages/front-page/front-page.ts:6