| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- import { expect, type Browser, type Page, test } from '@playwright/test'
- import {
- createAdminRole,
- createRoleWithEveryPermission,
- deleteRoles,
- membershipOf,
- ownerOrganizationId,
- setMembership,
- } from '../../support/db'
- import { settle } from '../../support/hydration'
- import { linkIn, waitForMail } from '../../support/mail'
- /**
- * Who may make somebody an admin.
- *
- * Inviting as admin is the owner's alone, and so is changing a member's
- * built-in role on the team page. The role picker's own action asked only
- * for admin standing, which a custom role can carry, so a person with such a
- * role could make themself, or anyone, a built-in admin, and from there edit
- * roles and remove members. The team page never offered them the picker; the
- * action behind it is what a browser can call, and what this file calls.
- *
- * The action's id is not written down anywhere a test could read it, so it is
- * taken from the request the owner's own click makes, and replayed with the
- * arguments of the caller's choosing, as a person in the browser could.
- */
- test.describe.configure({ mode: 'serial' })
- // Everybody starts as a stranger with no session.
- test.use({ storageState: { cookies: [], origins: [] } })
- const stamp = Date.now()
- const COLLEAGUE = `e2e-standing-${stamp}@example.com`
- const PEER = `e2e-peer-${stamp}@example.com`
- const PASSWORD = `E2e-pass-${stamp}`
- const baseURL = process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3100'
- let organizationId = ''
- let adminRoleId = ''
- let plainRoleId = ''
- /** The `Next-Action` id of assignRole, learnt from the owner's click. */
- let assignRoleAction = ''
- async function signIn(page: Page, email: string) {
- await page.goto('/auth/sign-in')
- await page.locator('#email').fill(email)
- await page.locator('#password').fill(PASSWORD)
- await page.getByRole('button', { name: 'Sign In', exact: true }).click()
- await page.waitForURL((url) => !url.pathname.startsWith('/auth'), { timeout: 30_000 })
- }
- /** The owner invites `email`, who signs up from the mail and lands in the workshop. */
- async function joinTeam(browser: Browser, email: string, name: string) {
- const owner = await browser.newContext({ storageState: 'e2e/.auth/owner.json' })
- const page = await owner.newPage()
- await page.goto('/settings/team')
- await settle(page)
- await expect(async () => {
- await page.getByRole('button', { name: 'Add', exact: true }).first().click()
- await expect(page.getByText('Someone in the office')).toBeVisible({ timeout: 2_000 })
- }).toPass({ timeout: 30_000 })
- await page.getByText('Someone in the office').click()
- await page.locator('#member-email').fill(email)
- await page.getByRole('button', { name: 'Invite', exact: true }).click()
- await expect(page.getByText(email).first()).toBeVisible({ timeout: 30_000 })
- await owner.close()
- const invitation = await waitForMail(email)
- const person = await browser.newContext({ storageState: { cookies: [], origins: [] } })
- const signup = await person.newPage()
- await signup.goto(linkIn(invitation, /\/auth\/sign-up\?invite=/))
- await signup.locator('#name').fill(name)
- await signup.locator('#email').fill(email)
- await signup.locator('#password').fill(PASSWORD)
- await signup.locator('#terms').click()
- await signup.getByRole('button', { name: /create account/i }).click()
- await signup.waitForURL((url) => !/^\/(auth|onboarding)/.test(url.pathname), {
- timeout: 30_000,
- })
- await person.close()
- }
- /**
- * Calls assignRole the way the page does: a POST to the page it lives on,
- * with the action id in the `Next-Action` header and the arguments as the
- * body. What the page would do with the answer does not matter here; the
- * membership is read back from the database.
- */
- async function callAssignRole(
- page: Page,
- args: { memberId: string; role: 'admin' | 'member'; roleId: string | null }
- ) {
- return page.request.post(`${baseURL}/settings/team`, {
- headers: {
- 'next-action': assignRoleAction,
- 'content-type': 'text/plain;charset=UTF-8',
- accept: 'text/x-component',
- origin: baseURL,
- },
- data: JSON.stringify([args]),
- })
- }
- test.beforeAll(async ({ browser }) => {
- organizationId = await ownerOrganizationId()
- await joinTeam(browser, COLLEAGUE, 'E2E Standing Colleague')
- await joinTeam(browser, PEER, 'E2E Peer')
- adminRoleId = await createAdminRole(organizationId, `E2E Admin Switch ${stamp}`)
- plainRoleId = await createRoleWithEveryPermission(organizationId, `E2E Plain ${stamp}`)
- })
- test.afterAll(async () => {
- // Two more ordinary members without a role, and the roles gone.
- if (organizationId) {
- for (const email of [COLLEAGUE, PEER]) {
- await setMembership(email, organizationId, { roleId: null, role: 'member' })
- }
- }
- await deleteRoles([adminRoleId, plainRoleId].filter(Boolean))
- })
- test.describe('the role picker’s action', () => {
- test('is learnt from the owner giving the peer a role', async ({ browser }) => {
- const owner = await browser.newContext({ storageState: 'e2e/.auth/owner.json' })
- const page = await owner.newPage()
- await page.goto('/settings/team')
- await settle(page)
- // Members are bordered cards, one per person, each with its own picker.
- // By test id: the page's own cards are bordered and rounded as well, and
- // one of them holds every member.
- const row = page.getByTestId('team-member').filter({ hasText: PEER })
- const action = page.waitForRequest((request) => Boolean(request.headers()['next-action']))
- await row.getByRole('combobox').click()
- await page.getByRole('option', { name: `E2E Plain ${stamp}`, exact: true }).click()
- assignRoleAction = (await action).headers()['next-action']
- expect(assignRoleAction).toBeTruthy()
- await expect
- .poll(async () => (await membershipOf(PEER, organizationId)).roleId)
- .toBe(plainRoleId)
- await owner.close()
- })
- test('does not let a custom-role admin make themself or a peer a built-in admin', async ({
- page,
- }) => {
- await setMembership(COLLEAGUE, organizationId, { roleId: adminRoleId, role: 'member' })
- await signIn(page, COLLEAGUE)
- const self = await membershipOf(COLLEAGUE, organizationId)
- const peer = await membershipOf(PEER, organizationId)
- await callAssignRole(page, { memberId: self.id, role: 'admin', roleId: null })
- await callAssignRole(page, { memberId: peer.id, role: 'admin', roleId: null })
- expect((await membershipOf(COLLEAGUE, organizationId)).role).toBe('member')
- expect(await membershipOf(PEER, organizationId)).toEqual(peer)
- // The action itself still works for them: a role that grants nothing on
- // its own is theirs to hand out.
- await callAssignRole(page, { memberId: peer.id, role: 'member', roleId: null })
- expect((await membershipOf(PEER, organizationId)).roleId).toBeNull()
- await callAssignRole(page, { memberId: peer.id, role: 'member', roleId: plainRoleId })
- expect((await membershipOf(PEER, organizationId)).roleId).toBe(plainRoleId)
- })
- test('does not let a built-in admin make a peer a built-in admin, or unmake one', async ({
- page,
- }) => {
- await setMembership(COLLEAGUE, organizationId, { roleId: null, role: 'admin' })
- await signIn(page, COLLEAGUE)
- const peer = await membershipOf(PEER, organizationId)
- await callAssignRole(page, { memberId: peer.id, role: 'admin', roleId: null })
- expect((await membershipOf(PEER, organizationId)).role).toBe('member')
- // Made an admin by the owner, the peer is out of the colleague's reach.
- await setMembership(PEER, organizationId, { roleId: null, role: 'admin' })
- await callAssignRole(page, { memberId: peer.id, role: 'member', roleId: plainRoleId })
- expect(await membershipOf(PEER, organizationId)).toEqual({
- id: peer.id,
- role: 'admin',
- roleId: null,
- })
- })
- test('lets the owner do both', async ({ browser }) => {
- const owner = await browser.newContext({ storageState: 'e2e/.auth/owner.json' })
- const page = await owner.newPage()
- await page.goto('/settings/team')
- const peer = await membershipOf(PEER, organizationId)
- await callAssignRole(page, { memberId: peer.id, role: 'member', roleId: plainRoleId })
- expect(await membershipOf(PEER, organizationId)).toEqual({
- id: peer.id,
- role: 'member',
- roleId: plainRoleId,
- })
- await callAssignRole(page, { memberId: peer.id, role: 'admin', roleId: null })
- expect((await membershipOf(PEER, organizationId)).role).toBe('admin')
- await owner.close()
- })
- })
|