feat: add organisations (#1820)

This commit is contained in:
David Nguyen
2025-06-10 11:49:52 +10:00
committed by GitHub
parent 0b37f19641
commit e6dc237ad2
631 changed files with 37616 additions and 25695 deletions
@@ -8,15 +8,17 @@ import { seedUser } from '@documenso/prisma/seed/users';
test.describe('Document API', () => {
test('sendDocument: should respect sendCompletionEmails setting', async ({ request }) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { document } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['signer@example.com'],
teamId: team.id,
});
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -81,11 +83,12 @@ test.describe('Document API', () => {
test('sendDocument: should not modify email settings when sendCompletionEmails is not provided', async ({
request,
}) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { document } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['signer@example.com'],
teamId: team.id,
});
// Set initial email settings
@@ -109,6 +112,7 @@ test.describe('Document API', () => {
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -1,281 +0,0 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import {
ZFindTeamMembersResponseSchema,
ZSuccessfulInviteTeamMemberResponseSchema,
ZSuccessfulRemoveTeamMemberResponseSchema,
ZSuccessfulUpdateTeamMemberResponseSchema,
ZUnsuccessfulResponseSchema,
} from '@documenso/api/v1/schema';
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 { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
test.describe('Team API', () => {
test('findTeamMembers: should list team members', async ({ request }) => {
const team = await seedTeam({
createTeamMembers: 3,
});
const ownerMember = team.members.find((member) => member.userId === team.owner.id)!;
// Should not be undefined
expect(ownerMember).toBeTruthy();
const { token } = await createApiToken({
userId: team.owner.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const response = await request.get(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const data = await response.json();
const parsed = ZFindTeamMembersResponseSchema.safeParse(data);
const safeData = parsed.success ? parsed.data : null;
expect(parsed.success).toBeTruthy();
expect(safeData!.members).toHaveLength(4); // Owner + 3 members
expect(safeData!.members[0]).toHaveProperty('id');
expect(safeData!.members[0]).toHaveProperty('email');
expect(safeData!.members[0]).toHaveProperty('role');
expect(safeData!.members).toContainEqual({
id: ownerMember.id,
email: ownerMember.user.email,
role: ownerMember.role,
});
});
test('inviteTeamMember: should invite a new team member', async ({ request }) => {
const team = await seedTeam();
const { token } = await createApiToken({
userId: team.owner.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const newUser = await seedUser();
const response = await request.post(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/invite`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
email: newUser.email,
role: TeamMemberRole.MEMBER,
},
},
);
expect(response.ok()).toBeTruthy();
const data = await response.json();
const parsed = ZSuccessfulInviteTeamMemberResponseSchema.safeParse(data);
const safeData = parsed.success ? parsed.data : null;
expect(parsed.success).toBeTruthy();
expect(safeData!.message).toBe('An invite has been sent to the member');
const invite = await prisma.teamMemberInvite.findFirst({
where: {
email: newUser.email,
teamId: team.id,
},
});
expect(invite).toBeTruthy();
});
test('updateTeamMember: should update a team member role', async ({ request }) => {
const team = await seedTeam({
createTeamMembers: 3,
});
const { token } = await createApiToken({
userId: team.owner.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const member = team.members.find((member) => member.role === TeamMemberRole.MEMBER)!;
// Should not be undefined
expect(member).toBeTruthy();
const response = await request.put(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${member.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
role: TeamMemberRole.ADMIN,
},
},
);
expect(response.ok()).toBeTruthy();
const data = await response.json();
const parsed = ZSuccessfulUpdateTeamMemberResponseSchema.safeParse(data);
const safeData = parsed.success ? parsed.data : null;
expect(parsed.success).toBeTruthy();
expect(safeData!.id).toBe(member.id);
expect(safeData!.email).toBe(member.user.email);
expect(safeData!.role).toBe(TeamMemberRole.ADMIN);
});
test('removeTeamMember: should remove a team member', async ({ request }) => {
const team = await seedTeam({
createTeamMembers: 3,
});
const { token } = await createApiToken({
userId: team.owner.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const member = team.members.find((member) => member.role === TeamMemberRole.MEMBER)!;
// Should not be undefined
expect(member).toBeTruthy();
const response = await request.delete(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${member.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
expect(response.status()).toBe(200);
const data = await response.json();
const parsed = ZSuccessfulRemoveTeamMemberResponseSchema.safeParse(data);
const safeData = parsed.success ? parsed.data : null;
expect(parsed.success).toBeTruthy();
expect(safeData!.id).toBe(member.id);
expect(safeData!.email).toBe(member.user.email);
expect(safeData!.role).toBe(member.role);
const removedMemberCount = await prisma.teamMember.count({
where: {
id: member.id,
teamId: team.id,
},
});
expect(removedMemberCount).toBe(0);
});
test('removeTeamMember: should not remove team owner', async ({ request }) => {
const team = await seedTeam({
createTeamMembers: 3,
});
const { token } = await createApiToken({
userId: team.owner.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const ownerMember = team.members.find((member) => member.userId === team.owner.id)!;
// Should not be undefined
expect(ownerMember).toBeTruthy();
const response = await request.delete(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${ownerMember.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
expect(response.status()).toBe(403);
const parsed = ZUnsuccessfulResponseSchema.safeParse(await response.json());
expect(parsed.success).toBeTruthy();
});
test('removeTeamMember: should not remove self', async ({ request }) => {
const team = await seedTeam({
createTeamMembers: 3,
});
const member = team.members.find((member) => member.role === TeamMemberRole.MEMBER)!;
// Make our non-owner member an admin
await prisma.teamMember.update({
where: {
id: member.id,
},
data: {
role: TeamMemberRole.ADMIN,
},
});
const { token } = await createApiToken({
userId: member.userId,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const response = await request.delete(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${member.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
expect(response.status()).toBe(403);
const parsed = ZUnsuccessfulResponseSchema.safeParse(await response.json());
expect(parsed.success).toBeTruthy();
});
});
@@ -18,17 +18,18 @@ test.describe('Template Field Prefill API v1', () => {
request,
}) => {
// 1. Create a user
const user = await seedUser();
const { user, team } = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
const template = await seedBlankTemplate(user, team.id, {
createTemplateOptions: {
title: 'Template with Advanced Fields',
},
@@ -349,17 +350,18 @@ test.describe('Template Field Prefill API v1', () => {
request,
}) => {
// 1. Create a user
const user = await seedUser();
const { user, team } = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
const template = await seedBlankTemplate(user, team.id, {
createTemplateOptions: {
title: 'Template with Default Fields',
},
@@ -519,17 +521,18 @@ test.describe('Template Field Prefill API v1', () => {
test('should handle invalid field prefill values', async ({ request }) => {
// 1. Create a user
const user = await seedUser();
const { user, team } = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template using seedBlankTemplate
const template = await seedBlankTemplate(user, {
const template = await seedBlankTemplate(user, team.id, {
createTemplateOptions: {
title: 'Template for Invalid Test',
visibility: 'EVERYONE',
@@ -8,10 +8,11 @@ test.describe('Embedding Presign API', () => {
test('createEmbeddingPresignToken: should create a token with default expiration', async ({
request,
}) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -44,10 +45,11 @@ test.describe('Embedding Presign API', () => {
test('createEmbeddingPresignToken: should create a token with custom expiration', async ({
request,
}) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -81,10 +83,11 @@ test.describe('Embedding Presign API', () => {
test.skip('createEmbeddingPresignToken: should create a token with immediate expiration in dev mode', async ({
request,
}) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -116,10 +119,11 @@ test.describe('Embedding Presign API', () => {
});
test('verifyEmbeddingPresignToken: should verify a valid token', async ({ request }) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -170,10 +174,11 @@ test.describe('Embedding Presign API', () => {
});
test('verifyEmbeddingPresignToken: should reject an invalid token', async ({ request }) => {
const user = await seedUser();
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
@@ -18,17 +18,18 @@ test.describe('Template Field Prefill API v2', () => {
request,
}) => {
// 1. Create a user
const user = await seedUser();
const { user, team } = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
const template = await seedBlankTemplate(user, team.id, {
createTemplateOptions: {
title: 'Template with Advanced Fields V2',
},
@@ -346,17 +347,18 @@ test.describe('Template Field Prefill API v2', () => {
request,
}) => {
// 1. Create a user
const user = await seedUser();
const { user, team } = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
const template = await seedBlankTemplate(user, team.id, {
createTemplateOptions: {
title: 'Template with Default Fields V2',
},
@@ -511,17 +513,18 @@ test.describe('Template Field Prefill API v2', () => {
test('should handle invalid field prefill values', async ({ request }) => {
// 1. Create a user
const user = await seedUser();
const { user, team } = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template using seedBlankTemplate
const template = await seedBlankTemplate(user, {
const template = await seedBlankTemplate(user, team.id, {
createTemplateOptions: {
title: 'Template for Invalid Test V2',
visibility: 'EVERYONE',