import { expect, test } from '@playwright/test'; import { contextForUser, latestMailFor, tokenFromMail } from './helpers'; /** * Peer invitations (issue #332), the full loop through the UI: a user * invites an address, registration is closed, the invitee registers * through the mailed link anyway, verifies, and the inviter sees the * invitation accepted. Needs Mailpit like the auth pack. */ const MAILPIT_URL = process.env.E2E_MAILPIT_URL; test.skip(!MAILPIT_URL, 'requires a Mailpit instance (E2E_MAILPIT_URL)'); const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; test('invite -> closed registration -> signup through the link -> accepted', async ({ browser, page, }) => { const stamp = Date.now().toString(36); const invitee = `invited-${stamp}@dorfteich.test`; const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const inviter = await contextForUser(browser, BASE_URL, 'fixture-user'); await admin.request.patch('/api/v1/admin/settings', { data: { 'auth.registrationMode': 'closed' }, }); try { // Invite through the settings UI. const settingsPage = await inviter.newPage(); await settingsPage.goto('/settings'); const section = settingsPage.locator('.invitations'); await section.getByLabel(/e-mail/i).fill(invitee); await section.getByRole('button', { name: /^(invite|einladen)$/i }).click(); await expect(section.locator('.invitations__sent')).toHaveText(/sent|verschickt/i); const row = section.locator(`.invitation-row[data-email="${invitee}"]`); await expect(row.locator('.invitation-row__status')).toHaveText(/open|offen/i); // Plain signup is closed… await page.goto('/signup'); await expect(page.locator('.form-banner')).toHaveText(/closed|geschlossen/i); // …but the mailed link opens the form, inviter banner and prefill included. const mail = await latestMailFor(MAILPIT_URL!, invitee); const invitationToken = /invitation=([A-Za-z0-9_-]+)/.exec(mail.text)?.[1]; expect(invitationToken).toBeTruthy(); await page.goto(`/signup?invitation=${invitationToken}`); await expect(page.locator('.signup-invitation__banner')).toBeVisible(); await expect(page.getByLabel(/e-mail/i)).toHaveValue(invitee); const username = `invited-${stamp}`; await page.getByLabel(/username|benutzername/i).fill(username); await page.getByLabel(/display name|anzeigename/i).fill('Invited Guest'); await page.getByLabel(/^password|^passwort/i).fill('ein einladungs passwort 1'); await page.getByRole('button', { name: /register|registrieren/i }).click(); await expect(page.getByRole('heading', { name: /inbox|postfach/i })).toBeVisible(); // The usual verification still applies (the link proves nothing about // the mailbox). Two mails went to this address — poll for the second. let verifyToken = ''; await expect(async () => { const verifyMail = await latestMailFor(MAILPIT_URL!, invitee); expect(verifyMail.text).toContain('/verify-email'); verifyToken = tokenFromMail(verifyMail.text); }).toPass(); await page.goto(`/verify-email?token=${verifyToken}`); await expect(page.getByRole('heading', { name: /confirmed|bestätigt/i })).toBeVisible(); // The inviter sees the acceptance; the used link is dead. await settingsPage.reload(); await expect( settingsPage .locator(`.invitation-row[data-email="${invitee}"]`) .locator('.invitation-row__status'), ).toHaveText(/accepted|angenommen/i); await page.goto(`/signup?invitation=${invitationToken}`); await expect(page.locator('.signup-invitation__invalid')).toBeVisible(); // Revoke flow through the UI: a second invitation dies by revoke. const second = `revoked-${stamp}@dorfteich.test`; await section.getByLabel(/e-mail/i).fill(second); await section.getByRole('button', { name: /^(invite|einladen)$/i }).click(); const secondRow = section.locator(`.invitation-row[data-email="${second}"]`); await expect(secondRow.locator('.invitation-row__status')).toHaveText(/open|offen/i); await secondRow.getByRole('button', { name: /revoke|widerrufen/i }).click(); await expect(secondRow.locator('.invitation-row__status')).toHaveText(/revoked|widerrufen/i); } finally { await admin.request.patch('/api/v1/admin/settings', { data: { 'auth.registrationMode': 'open' }, }); await admin.close(); await inviter.close(); } });