mirror of
https://github.com/documenso/documenso.git
synced 2026-07-27 02:15:05 +10:00
269 lines
10 KiB
TypeScript
269 lines
10 KiB
TypeScript
import { encryptEmailTransportConfig } from '@documenso/lib/server-only/email/email-transport-config';
|
|
import { generateDatabaseId, nanoid } from '@documenso/lib/universal/id';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
import { expect, type Locator, type Page, test } from '@playwright/test';
|
|
|
|
import { apiSignin } from '../../fixtures/authentication';
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
|
|
// ─── Cleanup ─────────────────────────────────────────────────────────────────
|
|
|
|
// Transports seeded by the current test, deleted afterwards. Deleting a transport
|
|
// referenced by a claim is safe: the FK is `onDelete: SetNull`.
|
|
const transportIdsToCleanup: string[] = [];
|
|
|
|
test.afterEach(async () => {
|
|
if (transportIdsToCleanup.length > 0) {
|
|
await prisma.emailTransport.deleteMany({ where: { id: { in: transportIdsToCleanup } } });
|
|
transportIdsToCleanup.length = 0;
|
|
}
|
|
});
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
const seedTransport = async (label: string) => {
|
|
const transport = await prisma.emailTransport.create({
|
|
data: {
|
|
id: generateDatabaseId('email_transport'),
|
|
name: `e2e-transport-${label}-${nanoid()}`,
|
|
type: 'RESEND',
|
|
fromName: 'Seeded Transport',
|
|
fromAddress: 'seeded@example.com',
|
|
config: encryptEmailTransportConfig({ type: 'RESEND', apiKey: `re_${nanoid()}` }),
|
|
},
|
|
});
|
|
|
|
transportIdsToCleanup.push(transport.id);
|
|
|
|
return transport;
|
|
};
|
|
|
|
const seedSubscriptionClaim = (name: string) =>
|
|
prisma.subscriptionClaim.create({
|
|
data: {
|
|
name,
|
|
teamCount: 1,
|
|
memberCount: 1,
|
|
envelopeItemCount: 10,
|
|
recipientCount: 10,
|
|
flags: {},
|
|
documentRateLimits: [],
|
|
emailRateLimits: [],
|
|
apiRateLimits: [],
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Seeds an organisation whose `OrganisationClaim` is descended (via
|
|
* `originalSubscriptionClaimId`) from the supplied subscription claim. This is
|
|
* the relationship the backport `updateMany` keys on.
|
|
*/
|
|
const seedOrgForClaim = async (subscriptionClaimId: string) => {
|
|
const { organisation } = await seedUser();
|
|
|
|
await prisma.organisationClaim.update({
|
|
where: { id: organisation.organisationClaim.id },
|
|
data: {
|
|
originalSubscriptionClaimId: subscriptionClaimId,
|
|
emailTransportId: null,
|
|
},
|
|
});
|
|
|
|
return organisation;
|
|
};
|
|
|
|
const openClaimUpdateDialog = async (page: Page, claimName: string) => {
|
|
// The update dialog lives inside the table row. Wait for the debounced search
|
|
// refetch to land BEFORE opening it, otherwise the table re-renders mid-flow
|
|
// and unmounts the dialog.
|
|
const searchSettled = page
|
|
.waitForResponse((r) => r.url().includes('claims.find') && r.url().includes(claimName), { timeout: 15_000 })
|
|
.catch(() => undefined);
|
|
|
|
await page.getByPlaceholder('Search by claim ID or name').fill(claimName);
|
|
await searchSettled;
|
|
|
|
const row = page.getByRole('row', { name: claimName });
|
|
await expect(row).toBeVisible();
|
|
|
|
// The actions dropdown trigger is the last button in the row (the first is the
|
|
// ID copy button).
|
|
await row.getByRole('button').last().click();
|
|
await page.getByRole('menuitem', { name: 'Update' }).click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog.getByRole('heading', { name: 'Update Subscription Claim' })).toBeVisible();
|
|
|
|
return dialog;
|
|
};
|
|
|
|
/**
|
|
* Picks an option from an open Radix Select listbox. The email-transport list is
|
|
* populated by a `find` query that can keep re-rendering (it loads up to 100
|
|
* transports), so the target option's box may still be shifting — wait for it,
|
|
* best-effort scroll it into view, then force the click.
|
|
*/
|
|
const chooseOption = async (page: Page, name: string) => {
|
|
const option = page.getByRole('option', { name });
|
|
await option.waitFor({ state: 'visible' });
|
|
await option.scrollIntoViewIfNeeded().catch(() => undefined);
|
|
await option.click({ force: true });
|
|
};
|
|
|
|
const selectEmailTransport = async (page: Page, dialog: Locator, transportName: string) => {
|
|
await dialog.getByRole('combobox').filter({ hasText: 'Default (system mailer)' }).click();
|
|
await chooseOption(page, transportName);
|
|
};
|
|
|
|
// ─── Subscription claim: NO backport ─────────────────────────────────────────
|
|
|
|
test('[ADMIN][EMAIL_TRANSPORT]: updating a subscription claim WITHOUT backport does not touch organisation claims', async ({
|
|
page,
|
|
}) => {
|
|
const { user: adminUser } = await seedUser({ isAdmin: true });
|
|
const transport = await seedTransport('no-backport');
|
|
const claimName = `e2e-claim-no-backport-${nanoid()}`;
|
|
const claim = await seedSubscriptionClaim(claimName);
|
|
const organisation = await seedOrgForClaim(claim.id);
|
|
|
|
await apiSignin({ page, email: adminUser.email, redirectPath: '/admin/claims' });
|
|
|
|
const dialog = await openClaimUpdateDialog(page, claimName);
|
|
|
|
await selectEmailTransport(page, dialog, transport.name);
|
|
|
|
// Backport checkbox left UNCHECKED.
|
|
await expect(dialog.getByRole('checkbox', { name: 'Backport email transport' })).not.toBeChecked();
|
|
|
|
await dialog.getByRole('button', { name: 'Update Claim' }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
|
|
// The subscription claim itself was updated (proves the mutation ran).
|
|
await expect
|
|
.poll(async () => {
|
|
const updated = await prisma.subscriptionClaim.findUniqueOrThrow({ where: { id: claim.id } });
|
|
return updated.emailTransportId;
|
|
})
|
|
.toBe(transport.id);
|
|
|
|
// The organisation claim was NOT backported.
|
|
const orgClaim = await prisma.organisationClaim.findFirstOrThrow({
|
|
where: { id: organisation.organisationClaim.id },
|
|
});
|
|
expect(orgClaim.emailTransportId).toBeNull();
|
|
});
|
|
|
|
// ─── Subscription claim: WITH backport ───────────────────────────────────────
|
|
|
|
test('[ADMIN][EMAIL_TRANSPORT]: updating a subscription claim WITH backport propagates to organisation claims', async ({
|
|
page,
|
|
}) => {
|
|
const { user: adminUser } = await seedUser({ isAdmin: true });
|
|
const transport = await seedTransport('backport');
|
|
const claimName = `e2e-claim-backport-${nanoid()}`;
|
|
const claim = await seedSubscriptionClaim(claimName);
|
|
const organisation = await seedOrgForClaim(claim.id);
|
|
|
|
await apiSignin({ page, email: adminUser.email, redirectPath: '/admin/claims' });
|
|
|
|
const dialog = await openClaimUpdateDialog(page, claimName);
|
|
|
|
await selectEmailTransport(page, dialog, transport.name);
|
|
|
|
// Enable backporting.
|
|
const backportCheckbox = dialog.getByRole('checkbox', { name: 'Backport email transport' });
|
|
await backportCheckbox.click();
|
|
await expect(backportCheckbox).toBeChecked();
|
|
|
|
await dialog.getByRole('button', { name: 'Update Claim' }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
|
|
// Both the subscription claim AND the descendant organisation claim are updated.
|
|
await expect
|
|
.poll(async () => {
|
|
const updated = await prisma.subscriptionClaim.findUniqueOrThrow({ where: { id: claim.id } });
|
|
return updated.emailTransportId;
|
|
})
|
|
.toBe(transport.id);
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const orgClaim = await prisma.organisationClaim.findFirstOrThrow({
|
|
where: { id: organisation.organisationClaim.id },
|
|
});
|
|
return orgClaim.emailTransportId;
|
|
})
|
|
.toBe(transport.id);
|
|
});
|
|
|
|
// ─── Organisation claim transport (set directly on the org page) ─────────────
|
|
|
|
test('[ADMIN][EMAIL_TRANSPORT]: setting the email transport on an organisation claim persists', async ({ page }) => {
|
|
const { user: adminUser } = await seedUser({ isAdmin: true });
|
|
const transport = await seedTransport('org-claim');
|
|
const { organisation } = await seedUser();
|
|
|
|
// Ensure a known starting point.
|
|
await prisma.organisationClaim.update({
|
|
where: { id: organisation.organisationClaim.id },
|
|
data: { emailTransportId: null },
|
|
});
|
|
|
|
await apiSignin({ page, email: adminUser.email, redirectPath: `/admin/organisations/${organisation.id}` });
|
|
|
|
// Scope to the billing/claims form (the one containing the "Email transport" field);
|
|
// the page has a second form (name/url) with its own "Update" button.
|
|
const billingForm = page.locator('form', { has: page.getByText('Email transport', { exact: true }) });
|
|
|
|
await billingForm.getByRole('combobox').filter({ hasText: 'Default (system mailer)' }).click();
|
|
await chooseOption(page, transport.name);
|
|
|
|
await billingForm.getByRole('button', { name: 'Update', exact: true }).click();
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const orgClaim = await prisma.organisationClaim.findFirstOrThrow({
|
|
where: { id: organisation.organisationClaim.id },
|
|
});
|
|
return orgClaim.emailTransportId;
|
|
})
|
|
.toBe(transport.id);
|
|
});
|
|
|
|
// ─── Organisation claim transport can be reset to the system mailer ──────────
|
|
|
|
test('[ADMIN][EMAIL_TRANSPORT]: clearing an organisation claim transport resets it to the system mailer', async ({
|
|
page,
|
|
}) => {
|
|
const { user: adminUser } = await seedUser({ isAdmin: true });
|
|
const transport = await seedTransport('org-clear');
|
|
const { organisation } = await seedUser();
|
|
|
|
// Start with the transport already assigned.
|
|
await prisma.organisationClaim.update({
|
|
where: { id: organisation.organisationClaim.id },
|
|
data: { emailTransportId: transport.id },
|
|
});
|
|
|
|
await apiSignin({ page, email: adminUser.email, redirectPath: `/admin/organisations/${organisation.id}` });
|
|
|
|
const billingForm = page.locator('form', { has: page.getByText('Email transport', { exact: true }) });
|
|
|
|
// The select currently shows the transport name; switch back to the default.
|
|
await billingForm.getByRole('combobox').filter({ hasText: transport.name }).click();
|
|
await chooseOption(page, 'Default (system mailer)');
|
|
|
|
await billingForm.getByRole('button', { name: 'Update', exact: true }).click();
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const orgClaim = await prisma.organisationClaim.findFirstOrThrow({
|
|
where: { id: organisation.organisationClaim.id },
|
|
});
|
|
return orgClaim.emailTransportId;
|
|
})
|
|
.toBeNull();
|
|
});
|