refactor(core): Update invitation endpoints to use DTOs (#12377)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-12-26 18:24:14 +01:00
committed by GitHub
parent 371a09de96
commit 7b2630d1a0
14 changed files with 282 additions and 171 deletions

View File

@@ -5,6 +5,9 @@ export { AiApplySuggestionRequestDto } from './ai/ai-apply-suggestion-request.dt
export { LoginRequestDto } from './auth/login-request.dto';
export { ResolveSignupTokenQueryDto } from './auth/resolve-signup-token-query.dto';
export { InviteUsersRequestDto } from './invitation/invite-users-request.dto';
export { AcceptInvitationRequestDto } from './invitation/accept-invitation-request.dto';
export { OwnerSetupRequestDto } from './owner/owner-setup-request.dto';
export { DismissBannerRequestDto } from './owner/dismiss-banner-request.dto';

View File

@@ -0,0 +1,94 @@
import { AcceptInvitationRequestDto } from '../accept-invitation-request.dto';
describe('AcceptInvitationRequestDto', () => {
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
describe('Valid requests', () => {
test.each([
{
name: 'complete valid invitation acceptance',
request: {
inviterId: validUuid,
firstName: 'John',
lastName: 'Doe',
password: 'SecurePassword123',
},
},
])('should validate $name', ({ request }) => {
const result = AcceptInvitationRequestDto.safeParse(request);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'missing inviterId',
request: {
firstName: 'John',
lastName: 'Doe',
password: 'SecurePassword123',
},
expectedErrorPath: ['inviterId'],
},
{
name: 'invalid inviterId',
request: {
inviterId: 'not-a-valid-uuid',
firstName: 'John',
lastName: 'Doe',
password: 'SecurePassword123',
},
expectedErrorPath: ['inviterId'],
},
{
name: 'missing first name',
request: {
inviterId: validUuid,
firstName: '',
lastName: 'Doe',
password: 'SecurePassword123',
},
expectedErrorPath: ['firstName'],
},
{
name: 'missing last name',
request: {
inviterId: validUuid,
firstName: 'John',
lastName: '',
password: 'SecurePassword123',
},
expectedErrorPath: ['lastName'],
},
{
name: 'password too short',
request: {
inviterId: validUuid,
firstName: 'John',
lastName: 'Doe',
password: 'short',
},
expectedErrorPath: ['password'],
},
{
name: 'password without number',
request: {
inviterId: validUuid,
firstName: 'John',
lastName: 'Doe',
password: 'NoNumberPassword',
},
expectedErrorPath: ['password'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = AcceptInvitationRequestDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View File

@@ -0,0 +1,60 @@
import { InviteUsersRequestDto } from '../invite-users-request.dto';
describe('InviteUsersRequestDto', () => {
describe('Valid requests', () => {
test.each([
{
name: 'empty array',
request: [],
},
{
name: 'single user invitation with default role',
request: [{ email: 'user@example.com' }],
},
{
name: 'multiple user invitations with different roles',
request: [
{ email: 'user1@example.com', role: 'global:member' },
{ email: 'user2@example.com', role: 'global:admin' },
],
},
])('should validate $name', ({ request }) => {
const result = InviteUsersRequestDto.safeParse(request);
expect(result.success).toBe(true);
});
it('should default role to global:member', () => {
const result = InviteUsersRequestDto.safeParse([{ email: 'user@example.com' }]);
expect(result.success).toBe(true);
expect(result.data?.[0].role).toBe('global:member');
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'invalid email',
request: [{ email: 'invalid-email' }],
expectedErrorPath: [0, 'email'],
},
{
name: 'invalid role',
request: [
{
email: 'user@example.com',
role: 'invalid-role',
},
],
expectedErrorPath: [0, 'role'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = InviteUsersRequestDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View File

@@ -0,0 +1,11 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { passwordSchema } from '../../schemas/password.schema';
export class AcceptInvitationRequestDto extends Z.class({
inviterId: z.string().uuid(),
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
password: passwordSchema,
}) {}

View File

@@ -0,0 +1,16 @@
import { z } from 'zod';
const roleSchema = z.enum(['global:member', 'global:admin']);
const invitedUserSchema = z.object({
email: z.string().email(),
role: roleSchema.default('global:member'),
});
const invitationsSchema = z.array(invitedUserSchema);
export class InviteUsersRequestDto extends Array<z.infer<typeof invitedUserSchema>> {
static safeParse(data: unknown) {
return invitationsSchema.safeParse(data);
}
}

View File

@@ -5,6 +5,8 @@ export type * from './scaling';
export type * from './frontend-settings';
export type * from './user';
export type { BannerName } from './schemas/bannerName.schema';
export type { Collaborator } from './push/collaboration';
export type { SendWorkerStatusMessage } from './push/worker';
export type { BannerName } from './schemas/bannerName.schema';
export { passwordSchema } from './schemas/password.schema';

View File

@@ -0,0 +1,54 @@
import { passwordSchema } from '../password.schema';
describe('passwordSchema', () => {
test('should throw on empty password', () => {
const check = () => passwordSchema.parse('');
expect(check).toThrowError('Password must be 8 to 64 characters long');
});
test('should return same password if valid', () => {
const validPassword = 'abcd1234X';
const validated = passwordSchema.parse(validPassword);
expect(validated).toBe(validPassword);
});
test('should require at least one uppercase letter', () => {
const invalidPassword = 'abcd1234';
const failingCheck = () => passwordSchema.parse(invalidPassword);
expect(failingCheck).toThrowError('Password must contain at least 1 uppercase letter.');
});
test('should require at least one number', () => {
const validPassword = 'abcd1234X';
const invalidPassword = 'abcdEFGH';
const validated = passwordSchema.parse(validPassword);
expect(validated).toBe(validPassword);
const check = () => passwordSchema.parse(invalidPassword);
expect(check).toThrowError('Password must contain at least 1 number.');
});
test('should require a minimum length of 8 characters', () => {
const invalidPassword = 'a'.repeat(7);
const check = () => passwordSchema.parse(invalidPassword);
expect(check).toThrowError('Password must be 8 to 64 characters long.');
});
test('should require a maximum length of 64 characters', () => {
const invalidPassword = 'a'.repeat(65);
const check = () => passwordSchema.parse(invalidPassword);
expect(check).toThrowError('Password must be 8 to 64 characters long.');
});
});