feat: add find envelopes endpoint (#2244)

This commit is contained in:
Ephraim Duncan
2025-12-23 11:51:51 +00:00
committed by GitHub
parent 790b385849
commit aa1cada79b
8 changed files with 813 additions and 4 deletions
@@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test';
import { type APIRequestContext, expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
@@ -27,6 +27,7 @@ import type {
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
import type { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
@@ -562,6 +563,200 @@ test.describe('API V2 Envelopes', () => {
});
});
test.describe('Envelope find endpoint', () => {
const createEnvelope = async (
request: APIRequestContext,
token: string,
payload: TCreateEnvelopePayload,
) => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const pdfData = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
);
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
return (await res.json()) as TCreateEnvelopeResponse;
};
test('should find envelopes with pagination', async ({ request }) => {
// Create 3 envelopes
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Document 1',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Document 2',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.TEMPLATE,
title: 'Template 1',
});
// Find all envelopes
const res = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TFindEnvelopesResponse;
expect(response.data.length).toBe(3);
expect(response.count).toBe(3);
expect(response.currentPage).toBe(1);
expect(response.totalPages).toBe(1);
// Test pagination
const paginatedRes = await request.get(`${baseUrl}/envelope?perPage=2&page=1`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(paginatedRes.ok()).toBeTruthy();
const paginatedResponse = (await paginatedRes.json()) as TFindEnvelopesResponse;
expect(paginatedResponse.data.length).toBe(2);
expect(paginatedResponse.count).toBe(3);
expect(paginatedResponse.totalPages).toBe(2);
});
test('should filter envelopes by type', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Document Only',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.TEMPLATE,
title: 'Template Only',
});
// Filter by DOCUMENT type
const documentRes = await request.get(`${baseUrl}/envelope?type=DOCUMENT`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(documentRes.ok()).toBeTruthy();
const documentResponse = (await documentRes.json()) as TFindEnvelopesResponse;
expect(documentResponse.data.every((e) => e.type === EnvelopeType.DOCUMENT)).toBe(true);
// Filter by TEMPLATE type
const templateRes = await request.get(`${baseUrl}/envelope?type=TEMPLATE`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(templateRes.ok()).toBeTruthy();
const templateResponse = (await templateRes.json()) as TFindEnvelopesResponse;
expect(templateResponse.data.every((e) => e.type === EnvelopeType.TEMPLATE)).toBe(true);
});
test('should filter envelopes by status', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Draft Document',
});
// Filter by DRAFT status (default for new envelopes)
const res = await request.get(`${baseUrl}/envelope?status=DRAFT`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const response = (await res.json()) as TFindEnvelopesResponse;
expect(response.data.every((e) => e.status === DocumentStatus.DRAFT)).toBe(true);
});
test('should search envelopes by query', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Unique Searchable Title',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Another Document',
});
const res = await request.get(`${baseUrl}/envelope?query=Unique%20Searchable`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const response = (await res.json()) as TFindEnvelopesResponse;
expect(response.data.length).toBe(1);
expect(response.data[0].title).toBe('Unique Searchable Title');
});
test('should not return envelopes from other users', async ({ request }) => {
// Create envelope for userA
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'UserA Document',
});
// Create envelope for userB
await createEnvelope(request, tokenB, {
type: EnvelopeType.DOCUMENT,
title: 'UserB Document',
});
// userA should only see their own envelopes
const resA = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(resA.ok()).toBeTruthy();
const responseA = (await resA.json()) as TFindEnvelopesResponse;
expect(responseA.data.every((e) => e.title !== 'UserB Document')).toBe(true);
// userB should only see their own envelopes
const resB = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeTruthy();
const responseB = (await resB.json()) as TFindEnvelopesResponse;
expect(responseB.data.every((e) => e.title !== 'UserA Document')).toBe(true);
});
test('should return envelope with expected schema fields', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Schema Test Document',
});
const res = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const response = (await res.json()) as TFindEnvelopesResponse;
const envelope = response.data.find((e) => e.title === 'Schema Test Document');
expect(envelope).toBeDefined();
expect(envelope?.id).toBeDefined();
expect(envelope?.type).toBe(EnvelopeType.DOCUMENT);
expect(envelope?.status).toBe(DocumentStatus.DRAFT);
expect(envelope?.recipients).toBeDefined();
expect(envelope?.user).toBeDefined();
expect(envelope?.team).toBeDefined();
});
});
test.describe('Empty recipient tests', () => {
test('Create template envelope with empty email recipient', async ({ request }) => {
const payload = {
@@ -12,14 +12,17 @@ import {
} from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
DocumentVisibility,
EnvelopeType,
FieldType,
FolderType,
Prisma,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
TeamMemberRole,
} from '@documenso/prisma/client';
import {
seedBlankDocument,
@@ -28,9 +31,11 @@ import {
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
import type {
TUseEnvelopePayload,
TUseEnvelopeResponse,
@@ -2990,6 +2995,278 @@ test.describe('Document API V2', () => {
});
});
test.describe('Envelope find endpoint', () => {
test('should block unauthorized access to envelope find endpoint', async ({ request }) => {
await seedBlankDocument(userA, teamA.id);
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const data = (await res.json()) as TFindEnvelopesResponse;
expect(data.data.every((doc) => doc.userId !== userA.id)).toBe(true);
});
test('should allow authorized access to envelope find endpoint', async ({ request }) => {
await seedBlankDocument(userA, teamA.id);
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const data = (await res.json()) as TFindEnvelopesResponse;
expect(data.data.length).toBeGreaterThan(0);
expect(data.data.some((doc) => doc.userId === userA.id)).toBe(true);
});
test('should respect team document visibility for ADMIN role', async ({ request }) => {
const adminMember = await seedTeamMember({
teamId: teamA.id,
role: TeamMemberRole.ADMIN,
});
const { token: adminToken } = await createApiToken({
userId: adminMember.id,
teamId: teamA.id,
tokenName: 'adminMember',
expiresIn: null,
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
visibility: DocumentVisibility.ADMIN,
title: 'Admin Only Document',
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
title: 'Manager and Above Document',
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
visibility: DocumentVisibility.EVERYONE,
title: 'Everyone Document',
},
});
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
headers: { Authorization: `Bearer ${adminToken}` },
});
expect(res.ok()).toBeTruthy();
const data = (await res.json()) as TFindEnvelopesResponse;
const titles = data.data.map((doc) => doc.title);
expect(titles).toContain('Admin Only Document');
expect(titles).toContain('Manager and Above Document');
expect(titles).toContain('Everyone Document');
});
test('should respect team document visibility for MANAGER role', async ({ request }) => {
const managerMember = await seedTeamMember({
teamId: teamA.id,
role: TeamMemberRole.MANAGER,
});
const { token: managerToken } = await createApiToken({
userId: managerMember.id,
teamId: teamA.id,
tokenName: 'managerMember',
expiresIn: null,
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
visibility: DocumentVisibility.ADMIN,
title: 'Admin Only Document',
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
title: 'Manager and Above Document',
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
visibility: DocumentVisibility.EVERYONE,
title: 'Everyone Document',
},
});
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
headers: { Authorization: `Bearer ${managerToken}` },
});
expect(res.ok()).toBeTruthy();
const data = (await res.json()) as TFindEnvelopesResponse;
const titles = data.data.map((doc) => doc.title);
expect(titles).not.toContain('Admin Only Document');
expect(titles).toContain('Manager and Above Document');
expect(titles).toContain('Everyone Document');
});
test('should filter envelopes by folderId with authorization', async ({ request }) => {
const folder = await prisma.folder.create({
data: {
userId: userA.id,
teamId: teamA.id,
name: 'Test Folder',
type: FolderType.DOCUMENT,
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
folderId: folder.id,
title: 'Document in Folder',
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
title: 'Document Not in Folder',
},
});
const resWithFolder = await request.get(
`${WEBAPP_BASE_URL}/api/v2-beta/envelope?folderId=${folder.id}`,
{
headers: { Authorization: `Bearer ${tokenA}` },
},
);
expect(resWithFolder.ok()).toBeTruthy();
const dataWithFolder = (await resWithFolder.json()) as TFindEnvelopesResponse;
expect(dataWithFolder.data.every((doc) => doc.folderId === folder.id)).toBe(true);
expect(dataWithFolder.data.some((doc) => doc.title === 'Document in Folder')).toBe(true);
const resUnauthorized = await request.get(
`${WEBAPP_BASE_URL}/api/v2-beta/envelope?folderId=${folder.id}`,
{
headers: { Authorization: `Bearer ${tokenB}` },
},
);
expect(resUnauthorized.ok()).toBeTruthy();
const dataUnauthorized = (await resUnauthorized.json()) as TFindEnvelopesResponse;
expect(
dataUnauthorized.data.every(
(doc) => doc.folderId !== folder.id || doc.userId !== userA.id,
),
).toBe(true);
});
test('should filter envelopes by type with authorization', async ({ request }) => {
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
type: EnvelopeType.DOCUMENT,
title: 'UserA Document',
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
type: EnvelopeType.TEMPLATE,
title: 'UserA Template',
},
});
await seedBlankDocument(userB, teamB.id, {
createDocumentOptions: {
type: EnvelopeType.DOCUMENT,
title: 'UserB Document',
},
});
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope?type=DOCUMENT`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const data = (await res.json()) as TFindEnvelopesResponse;
expect(data.data.every((doc) => doc.type === EnvelopeType.DOCUMENT)).toBe(true);
expect(data.data.every((doc) => doc.userId === userA.id)).toBe(true);
expect(data.data.some((doc) => doc.title === 'UserA Document')).toBe(true);
expect(data.data.every((doc) => doc.title !== 'UserB Document')).toBe(true);
});
test('should filter envelopes by status with authorization', async ({ request }) => {
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
title: 'Draft Document',
status: DocumentStatus.DRAFT,
},
});
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
title: 'Completed Document',
status: DocumentStatus.COMPLETED,
},
});
await seedBlankDocument(userB, teamB.id, {
createDocumentOptions: {
title: 'UserB Draft',
status: DocumentStatus.DRAFT,
},
});
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope?status=DRAFT`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const data = (await res.json()) as TFindEnvelopesResponse;
expect(data.data.every((doc) => doc.status === DocumentStatus.DRAFT)).toBe(true);
expect(data.data.every((doc) => doc.userId === userA.id)).toBe(true);
expect(data.data.some((doc) => doc.title === 'Draft Document')).toBe(true);
expect(data.data.every((doc) => doc.title !== 'UserB Draft')).toBe(true);
expect(data.data.every((doc) => doc.title !== 'Completed Document')).toBe(true);
});
test('should search envelopes by query with authorization', async ({ request }) => {
await seedBlankDocument(userA, teamA.id, {
createDocumentOptions: {
title: 'Unique Searchable Title UserA',
},
});
await seedBlankDocument(userB, teamB.id, {
createDocumentOptions: {
title: 'Unique Searchable Title UserB',
},
});
const res = await request.get(
`${WEBAPP_BASE_URL}/api/v2-beta/envelope?query=Unique%20Searchable`,
{
headers: { Authorization: `Bearer ${tokenA}` },
},
);
expect(res.ok()).toBeTruthy();
const data = (await res.json()) as TFindEnvelopesResponse;
expect(data.data.every((doc) => doc.userId === userA.id)).toBe(true);
expect(data.data.some((doc) => doc.title.includes('UserA'))).toBe(true);
expect(data.data.every((doc) => !doc.title.includes('UserB'))).toBe(true);
});
});
test.describe('Envelope update endpoint', () => {
test('should block unauthorized access to envelope update endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);