Skip to content

Auth · E2E tests

3 modules · 11 exports · 1 repository · 4 e2e tests · 8 routes

  • SSendou
  • Arrange data setup
  • Act what the user does
  • Assert what is expected

Auth

Renders//notifications/settings

Setup
Arrange
  1. Create log in link user N-ZAP → logInLink
    const logInLink = await factories.LogInLinkFactory.create({
    	userId: NZAP_TEST_ID,
    });
LOLogged out
Log in link page
Act
  1. Open the log in link page logInLink.code
    await logInLinkPage.goto(logInLink.code);
Assert
  1. the page URL is “/”
    await expect(page).toHaveURL("/");
Side nav
Assert
  1. footer username reads “N-ZAP”
    await expect(sideNav.locators.footerUsername).toHaveText("N-ZAP");
  2. log in button is not visible
    await isNotVisible(sideNav.locators.logInButton);
Settings page
Act
  1. Open the settings page
    await settings.goto();
  2. Log out
    await settings.logOut();
Side nav
Assert
  1. log in button is visible
    await expect(sideNav.locators.logInButton).toBeVisible();
  2. footer username is not visible
    await isNotVisible(sideNav.locators.footerUsername);
Act
  1. Go to NOTIFICATIONS_URL
    await navigate({ page, url: NOTIFICATIONS_URL });
Error page
Assert
  1. heading “Authentication required” is visible
    await expect(errorPage.heading("Authentication required")).toBeVisible();
Log in link page
Act
  1. the first log in consumed the single use link

    Fetch response logInLink.code → reusedLink
    const reusedLink = await logInLinkPage.fetchResponse(logInLink.code);
Assert
  1. 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

Setup
Arrange
  1. Create user KEEPER → keeper
    const keeper = await factories.UserFactory.create(KEEPER);
  2. Create user LEAVER → leaver
    const leaver = await factories.UserFactory.create(LEAVER);
  3. Create log in link user id leaver.id → leaverLogInLink
    const leaverLogInLink = await factories.LogInLinkFactory.create({
    	userId: leaver.id,
    });
  4. Create log in link user id keeper.id → keeperLogInLink
    const keeperLogInLink = await factories.LogInLinkFactory.create({
    	userId: keeper.id,
    });
LOLogged out
Log in link page
Act
  1. Open the log in link page leaverLogInLink.code
    await logInLinkPage.goto(leaverLogInLink.code);
Side nav
Assert
  1. footer username reads LEAVER.discordName
    await expect(sideNav.locators.footerUsername).toHaveText(
    	LEAVER.discordName,
    );
Act
  1. Open second user browser, workerBaseURL → admin
    const admin = await openSecondUser(browser, workerBaseURL);
Admin actions page
Act
  1. Open the admin actions page
    await adminActions.goto();
  2. Migrate user old user name KEEPER.discordName new user name LEAVER.discordName
    await adminActions.migrateUser({
    	oldUserName: KEEPER.discordName,
    	newUserName: LEAVER.discordName,
    });
  3. Close
    await admin.close();
Arrange
  1. 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
  1. Session cookie → staleCookie
    const staleCookie = await sessionCookie();
Assert
  1. staleCookie be defined
    expect(staleCookie).toBeDefined();
Act
  1. Go to “/”
    await navigate({ page, url: "/" });
Assert
  1. no error page
    await expectNoErrorPage(page);
Side nav
Assert
  1. log in button is visible
    await expect(sideNav.locators.logInButton).toBeVisible();
  2. footer username is not visible
    await isNotVisible(sideNav.locators.footerUsername);
  3. sessionCookie() be undefined
    expect(await sessionCookie()).toBeUndefined();
Act
  1. Go to NOTIFICATIONS_URL
    await navigate({ page, url: NOTIFICATIONS_URL });
Error page
Assert
  1. heading “Authentication required” is visible
    await expect(errorPage.heading("Authentication required")).toBeVisible();
Act
  1. Add cookies staleCookie!
    await page.context().addCookies([staleCookie!]);
Log in link page
Act
  1. Open the log in link page keeperLogInLink.code
    await logInLinkPage.goto(keeperLogInLink.code);
Side nav
Assert
  1. 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,
	);
})

log in button starts the Discord OAuth flow

Renders//auth

LOLogged out
Discord authorize interceptor
Act
  1. Install context
    await discord.install(context);
Front page
Act
  1. Open the front page
    await frontPage.goto();
Side nav
Act
  1. Click log in button
    await sideNav.locators.logInButton.click();
Discord authorize interceptor
Act
  1. Wait for capture
    await discord.waitForCapture();
Assert
  1. href match /discord\.com\/(api\/)?oauth2\/authorize/
    expect(discord.authorizeUrl.href).toMatch(
    	/discord\.com\/(api\/)?oauth2\/authorize/,
    );
  2. param “client_id” is “123”
    expect(discord.param("client_id")).toBe("123");
  3. param “response_type” is “code”
    expect(discord.param("response_type")).toBe("code");
  4. param “scope” contains “identify”
    expect(discord.param("scope")).toContain("identify");
  5. param “state” is truthy
    expect(discord.param("state")).toBeTruthy();
  6. 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

SSendou
Act
  1. Get “/cache-deception.css” → staticLookingNotFound
    const staticLookingNotFound = await page.request.get(
    	"/cache-deception.css",
    );
Assert
  1. staticLookingNotFound.status() is 404
    expect(staticLookingNotFound.status()).toBe(404);
  2. staticLookingNotFound.headers()["cache-control"] is “private, no-cache”
    expect(staticLookingNotFound.headers()["cache-control"]).toBe(
    	"private, no-cache",
    );
Act
  1. Get “/settings.data” → loaderData
    const loaderData = await page.request.get("/settings.data");
Assert
  1. loaderData.ok() is true
    expect(loaderData.ok()).toBe(true);
  2. 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");
})