mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 08:54:20 +10:00
chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/external-2fa-codes. Resolve formatting conflicts caused by biome rollout; preserve both feature streams: PR's external 2FA token + signing-session 2FA proof additions plus main's RateLimit/RecipientExpired/signingReminders/date-auto-insert. In complete-document-with-token.ts, drop the duplicate early field-fetching block introduced when main moved that logic later with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH check using derivedRecipientActionAuth.
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { seedPendingDocumentNoFields, seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
createDocumentAuthOptions,
|
||||
createRecipientAuthOptions,
|
||||
} from '@documenso/lib/utils/document-auth';
|
||||
import {
|
||||
seedPendingDocumentNoFields,
|
||||
seedPendingDocumentWithFullFields,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
@@ -106,9 +99,7 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
||||
});
|
||||
|
||||
// Currently document auth for signing/approving/viewing is not required.
|
||||
test.skip('[DOCUMENT_AUTH]: should deny signing document when required for global auth', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip('[DOCUMENT_AUTH]: should deny signing document when required for global auth', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { user: recipientWithAccount } = await seedUser();
|
||||
@@ -133,14 +124,10 @@ test.skip('[DOCUMENT_AUTH]: should deny signing document when required for globa
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await expect(page.getByRole('paragraph')).toContainText(
|
||||
'Reauthentication is required to sign the document',
|
||||
);
|
||||
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign the document');
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { user: recipientWithAccount } = await seedUser();
|
||||
@@ -170,17 +157,13 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
|
||||
}
|
||||
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
await expect(page.getByRole('paragraph')).toContainText(
|
||||
'Reauthentication is required to sign this field',
|
||||
);
|
||||
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign this field');
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient auth', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient auth', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { user: recipientWithInheritAuth } = await seedUser();
|
||||
@@ -190,11 +173,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
const { recipients } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
teamId: team.id,
|
||||
recipients: [
|
||||
recipientWithInheritAuth,
|
||||
recipientWithExplicitNoneAuth,
|
||||
recipientWithExplicitAccountAuth,
|
||||
],
|
||||
recipients: [recipientWithInheritAuth, recipientWithExplicitNoneAuth, recipientWithExplicitAccountAuth],
|
||||
recipientsCreateOptions: [
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
@@ -237,9 +216,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
}
|
||||
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
await expect(page.getByRole('paragraph')).toContainText(
|
||||
'Reauthentication is required to sign this field',
|
||||
);
|
||||
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign this field');
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
}
|
||||
|
||||
@@ -278,9 +255,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
}
|
||||
});
|
||||
|
||||
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient and global auth', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient and global auth', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { user: recipientWithInheritAuth } = await seedUser();
|
||||
@@ -290,11 +265,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
const { recipients } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
teamId: team.id,
|
||||
recipients: [
|
||||
recipientWithInheritAuth,
|
||||
recipientWithExplicitNoneAuth,
|
||||
recipientWithExplicitAccountAuth,
|
||||
],
|
||||
recipients: [recipientWithInheritAuth, recipientWithExplicitNoneAuth, recipientWithExplicitAccountAuth],
|
||||
recipientsCreateOptions: [
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
@@ -343,9 +314,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
}
|
||||
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
await expect(page.getByRole('paragraph')).toContainText(
|
||||
'Reauthentication is required to sign this field',
|
||||
);
|
||||
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign this field');
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { type APIRequestContext, expect, test } from '@playwright/test';
|
||||
import { FieldType, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { apiSeedPendingDocument } from '../fixtures/api-seeds';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
|
||||
type SeededEnvelopes = {
|
||||
assistantToken: string;
|
||||
otherEnvelopeFieldId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Seeds two unrelated pending envelopes:
|
||||
* - Envelope A has an ASSISTANT (with a token) plus a SIGNER.
|
||||
* - Envelope B is owned by a different user and has a SIGNER with a TEXT field.
|
||||
*
|
||||
* Returns the assistant's token from envelope A and the TEXT field id from
|
||||
* envelope B so callers can exercise signing routes across envelopes.
|
||||
*/
|
||||
const seedTwoPendingEnvelopes = async (request: APIRequestContext): Promise<SeededEnvelopes> => {
|
||||
const envelopeA = await apiSeedPendingDocument(request, {
|
||||
title: '[TEST] Envelope A',
|
||||
recipients: [
|
||||
{
|
||||
email: `assistant-${Date.now()}@documenso.com`,
|
||||
name: 'Assistant',
|
||||
role: 'ASSISTANT',
|
||||
signingOrder: 1,
|
||||
},
|
||||
{
|
||||
email: `signer-a-${Date.now()}@documenso.com`,
|
||||
name: 'Signer A',
|
||||
role: 'SIGNER',
|
||||
signingOrder: 2,
|
||||
},
|
||||
],
|
||||
fieldsPerRecipient: [
|
||||
[],
|
||||
// SIGNER needs a SIGNATURE field so distribution succeeds.
|
||||
[{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 5, width: 5, height: 5 }],
|
||||
],
|
||||
});
|
||||
|
||||
const assistant = envelopeA.distributeResult.recipients.find((r) => r.role === 'ASSISTANT');
|
||||
|
||||
if (!assistant) {
|
||||
throw new Error('Assistant recipient not found in envelope A');
|
||||
}
|
||||
|
||||
const envelopeB = await apiSeedPendingDocument(request, {
|
||||
title: '[TEST] Envelope B',
|
||||
recipients: [
|
||||
{
|
||||
email: `signer-b-${Date.now()}@documenso.com`,
|
||||
name: 'Signer B',
|
||||
role: 'SIGNER',
|
||||
signingOrder: 1,
|
||||
},
|
||||
],
|
||||
// A TEXT field is used as the cross-envelope target. The V2 route has a
|
||||
// separate guard that blocks assistants from signing SIGNATURE fields,
|
||||
// which would mask whether the recipient lookup itself was scoped.
|
||||
fieldsPerRecipient: [
|
||||
[
|
||||
{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 5, width: 5, height: 5 },
|
||||
{ type: FieldType.TEXT, page: 1, positionX: 5, positionY: 15, width: 5, height: 5 },
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const otherEnvelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: envelopeB.envelope.id },
|
||||
include: { fields: true },
|
||||
});
|
||||
|
||||
const textField = otherEnvelope.fields.find((f) => f.type === FieldType.TEXT);
|
||||
|
||||
if (!textField) {
|
||||
throw new Error('TEXT field not found in envelope B');
|
||||
}
|
||||
|
||||
return {
|
||||
assistantToken: assistant.token,
|
||||
otherEnvelopeFieldId: textField.id,
|
||||
};
|
||||
};
|
||||
|
||||
const trpcMutation = async (request: APIRequestContext, procedure: string, input: Record<string, unknown>) => {
|
||||
return await request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: JSON.stringify({ json: input }),
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('[ASSISTANT_SIGNING_AUTH]: cross-envelope field access', () => {
|
||||
test('envelope.field.sign (V2) rejects fieldId from another envelope', async ({ request }) => {
|
||||
const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request);
|
||||
|
||||
const res = await trpcMutation(request, 'envelope.field.sign', {
|
||||
token: assistantToken,
|
||||
fieldId: otherEnvelopeFieldId,
|
||||
fieldValue: { type: FieldType.TEXT, value: 'TEXT' },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
|
||||
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
||||
where: { id: otherEnvelopeFieldId },
|
||||
});
|
||||
|
||||
expect(fieldAfter.inserted).toBe(false);
|
||||
expect(fieldAfter.customText).toBe('');
|
||||
});
|
||||
|
||||
test('field.signFieldWithToken (V1) rejects fieldId from another envelope', async ({ request }) => {
|
||||
const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request);
|
||||
|
||||
const res = await trpcMutation(request, 'field.signFieldWithToken', {
|
||||
token: assistantToken,
|
||||
fieldId: otherEnvelopeFieldId,
|
||||
value: 'TEXT',
|
||||
isBase64: false,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
|
||||
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
||||
where: { id: otherEnvelopeFieldId },
|
||||
});
|
||||
|
||||
expect(fieldAfter.inserted).toBe(false);
|
||||
expect(fieldAfter.customText).toBe('');
|
||||
});
|
||||
|
||||
test('field.removeSignedFieldWithToken (V1) rejects fieldId from another envelope', async ({ request }) => {
|
||||
const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request);
|
||||
|
||||
// Pre-insert the field so a successful (incorrect) uninsert is detectable.
|
||||
await prisma.field.update({
|
||||
where: { id: otherEnvelopeFieldId },
|
||||
data: { inserted: true, customText: 'pre-existing-value' },
|
||||
});
|
||||
|
||||
const res = await trpcMutation(request, 'field.removeSignedFieldWithToken', {
|
||||
token: assistantToken,
|
||||
fieldId: otherEnvelopeFieldId,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
|
||||
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
||||
where: { id: otherEnvelopeFieldId },
|
||||
include: { recipient: true },
|
||||
});
|
||||
|
||||
expect(fieldAfter.inserted).toBe(true);
|
||||
expect(fieldAfter.customText).toBe('pre-existing-value');
|
||||
expect(fieldAfter.recipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,12 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentSigningOrder, DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { signDirectSignaturePad, signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
const { user: firstSigner } = await seedUser();
|
||||
const { user: secondSigner } = await seedUser();
|
||||
@@ -157,9 +148,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should not show dictation UI when disabled', a
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient UI is not shown
|
||||
await expect(
|
||||
page.getByText('The next recipient to sign this document will be'),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
|
||||
|
||||
// Submit and verify completion
|
||||
@@ -243,9 +232,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient UI is not shown in parallel flow
|
||||
await expect(
|
||||
page.getByText('The next recipient to sign this document will be'),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
|
||||
|
||||
// Submit and verify completion
|
||||
@@ -274,9 +261,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
const { user: assistant } = await seedUser();
|
||||
const { user: signer } = await seedUser();
|
||||
|
||||
Reference in New Issue
Block a user