mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
4f346d3c2d
Adds a CANCELLED envelope status that privileged members (owner or team admin/manager) can move a pending document into. Sending recipient notifications via a background job while retaining the document in the dashboard as proof of distribution. Includes a dedicated Cancelled tab, single and bulk cancel actions, the ENVELOPE_CANCELLED mutability guard, and e2e coverage for permissions and visibility.
243 lines
8.5 KiB
TypeScript
243 lines
8.5 KiB
TypeScript
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { seedCompletedDocument, seedDraftDocument, seedPendingDocument } from '@documenso/prisma/seed/documents';
|
|
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
import { expect, test } from '@playwright/test';
|
|
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
|
|
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
|
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
|
|
|
test.describe.configure({
|
|
mode: 'parallel',
|
|
});
|
|
|
|
const createTokenForUser = async (userId: number, teamId: number, tokenName: string) => {
|
|
const { token } = await createApiToken({
|
|
userId,
|
|
teamId,
|
|
tokenName,
|
|
expiresIn: null,
|
|
});
|
|
|
|
return token;
|
|
};
|
|
|
|
test.describe('Envelope cancel endpoint authorization', () => {
|
|
test('hides the document from an outsider attempting to cancel it', async ({ request }) => {
|
|
const { user: owner, team } = await seedUser();
|
|
const { user: recipient } = await seedUser();
|
|
const document = await seedPendingDocument(owner, team.id, [recipient]);
|
|
|
|
const { user: outsider, team: outsiderTeam } = await seedUser();
|
|
const outsiderToken = await createTokenForUser(outsider.id, outsiderTeam.id, 'outsider');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${outsiderToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
// Outsiders must not be able to determine whether the envelope exists.
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(404);
|
|
|
|
// The document must be untouched.
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.PENDING);
|
|
});
|
|
|
|
test('hides the document from a recipient attempting to cancel it', async ({ request }) => {
|
|
const { user: owner, team } = await seedUser();
|
|
const { user: recipient, team: recipientTeam } = await seedUser();
|
|
const document = await seedPendingDocument(owner, team.id, [recipient]);
|
|
|
|
const recipientToken = await createTokenForUser(recipient.id, recipientTeam.id, 'recipient');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${recipientToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
// A recipient is not a member of the document's team, so they must not be
|
|
// able to determine whether it exists via this endpoint.
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(404);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.PENDING);
|
|
});
|
|
|
|
// Note: a non-privileged MEMBER cannot obtain an API token at all (token
|
|
// creation requires the MANAGE_TEAM permission), so the MEMBER cancellation
|
|
// restriction is covered through the UI tests in cancel-documents.spec.ts
|
|
// rather than at the API layer.
|
|
|
|
test('allows the document owner to cancel a pending document', async ({ request }) => {
|
|
const { user: owner, team } = await seedUser();
|
|
const { user: recipient } = await seedUser();
|
|
const document = await seedPendingDocument(owner, team.id, [recipient]);
|
|
|
|
const ownerToken = await createTokenForUser(owner.id, team.id, 'owner');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${ownerToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
expect(res.status()).toBe(200);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true, completedAt: true, deletedAt: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.CANCELLED);
|
|
expect(documentInDb.completedAt).not.toBeNull();
|
|
expect(documentInDb.deletedAt).toBeNull();
|
|
});
|
|
|
|
test('allows a team ADMIN to cancel a pending document they do not own', async ({ request }) => {
|
|
const { team, owner } = await seedTeam();
|
|
|
|
const adminUser = await seedTeamMember({
|
|
teamId: team.id,
|
|
role: TeamMemberRole.ADMIN,
|
|
});
|
|
|
|
const { user: recipient } = await seedUser();
|
|
const document = await seedPendingDocument(owner, team.id, [recipient]);
|
|
|
|
const adminToken = await createTokenForUser(adminUser.id, team.id, 'admin');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
expect(res.status()).toBe(200);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.CANCELLED);
|
|
});
|
|
|
|
test('allows a team MANAGER to cancel a pending document they do not own', async ({ request }) => {
|
|
const { team, owner } = await seedTeam();
|
|
|
|
const managerUser = await seedTeamMember({
|
|
teamId: team.id,
|
|
role: TeamMemberRole.MANAGER,
|
|
});
|
|
|
|
const { user: recipient } = await seedUser();
|
|
const document = await seedPendingDocument(owner, team.id, [recipient]);
|
|
|
|
const managerToken = await createTokenForUser(managerUser.id, team.id, 'manager');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${managerToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
expect(res.status()).toBe(200);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.CANCELLED);
|
|
});
|
|
|
|
test('rejects cancelling a draft document', async ({ request }) => {
|
|
const { user: owner, team } = await seedUser();
|
|
const document = await seedDraftDocument(owner, team.id, []);
|
|
|
|
const ownerToken = await createTokenForUser(owner.id, team.id, 'owner-draft');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${ownerToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.DRAFT);
|
|
});
|
|
|
|
test('rejects cancelling a completed document', async ({ request }) => {
|
|
const { user: owner, team } = await seedUser();
|
|
const { user: recipient } = await seedUser();
|
|
const document = await seedCompletedDocument(owner, team.id, [recipient]);
|
|
|
|
const ownerToken = await createTokenForUser(owner.id, team.id, 'owner-completed');
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${ownerToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.COMPLETED);
|
|
});
|
|
|
|
test('rejects double cancellation of an already cancelled document', async ({ request }) => {
|
|
const { user: owner, team } = await seedUser();
|
|
const { user: recipient } = await seedUser();
|
|
const document = await seedPendingDocument(owner, team.id, [recipient]);
|
|
|
|
const ownerToken = await createTokenForUser(owner.id, team.id, 'owner-double');
|
|
|
|
const firstRes = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${ownerToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(firstRes.status()).toBe(200);
|
|
|
|
const secondRes = await request.post(`${baseUrl}/envelope/cancel`, {
|
|
headers: { Authorization: `Bearer ${ownerToken}` },
|
|
data: { envelopeId: document.id },
|
|
});
|
|
|
|
expect(secondRes.ok()).toBeFalsy();
|
|
expect(secondRes.status()).toBe(400);
|
|
|
|
const documentInDb = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
select: { status: true },
|
|
});
|
|
|
|
expect(documentInDb.status).toBe(DocumentStatus.CANCELLED);
|
|
});
|
|
});
|