mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add team user management endpoints to api (#1416)
## Description Adds user management capabilities to our current API. Allows for adding, removing, listing and updating members of a given team using a valid API token. ## Related Issue N/A ## Changes Made - Added an endpoint for inviting a team member - Added an endpoint for removing a team member - Added an endpoint for updating a team member - Added an endpoint for listing team members ## Testing Performed Tests were written for this feature request
This commit is contained in:
278
packages/app-tests/e2e/api/v1/team-user-management.spec.ts
Normal file
278
packages/app-tests/e2e/api/v1/team-user-management.spec.ts
Normal file
@ -0,0 +1,278 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
ZFindTeamMembersResponseSchema,
|
||||
ZSuccessfulInviteTeamMemberResponseSchema,
|
||||
ZSuccessfulRemoveTeamMemberResponseSchema,
|
||||
ZSuccessfulUpdateTeamMemberResponseSchema,
|
||||
ZUnsuccessfulResponseSchema,
|
||||
} from '@documenso/api/v1/schema';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
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(`${WEBAPP_BASE_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(
|
||||
`${WEBAPP_BASE_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(
|
||||
`${WEBAPP_BASE_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(
|
||||
`${WEBAPP_BASE_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(
|
||||
`${WEBAPP_BASE_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(
|
||||
`${WEBAPP_BASE_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();
|
||||
});
|
||||
});
|
||||
@ -5,9 +5,9 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test:dev": "playwright test",
|
||||
"test-ui:dev": "playwright test --ui",
|
||||
"test:e2e": "start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test\""
|
||||
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
|
||||
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
|
||||
"test:e2e": "NODE_OPTIONS=--experimental-require-module start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
Reference in New Issue
Block a user