diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 03dbc0c1..4d436250 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -378,6 +378,7 @@ export class AuthService { // A 6-digit verification code will be sent to their email const user = await this.userService.create({ ...dto, + lastName: dto.lastName ?? null, email: dto.email, role: role.id, status: { diff --git a/src/auth/dto/auth-register-login.dto.spec.ts b/src/auth/dto/auth-register-login.dto.spec.ts new file mode 100644 index 00000000..b4aef6fb --- /dev/null +++ b/src/auth/dto/auth-register-login.dto.spec.ts @@ -0,0 +1,93 @@ +import { validate } from 'class-validator'; +import { plainToInstance } from 'class-transformer'; +import { AuthRegisterLoginDto } from './auth-register-login.dto'; + +describe('AuthRegisterLoginDto', () => { + describe('validation', () => { + it('should validate successfully with valid data', async () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'test@example.com', + password: 'secret123', + firstName: 'John', + lastName: 'Doe', + }); + + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('should fail when email is invalid', async () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'not-an-email', + password: 'secret123', + firstName: 'John', + lastName: 'Doe', + }); + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe('email'); + }); + + it('should fail when password is too short', async () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'test@example.com', + password: '12345', // less than 6 characters + firstName: 'John', + lastName: 'Doe', + }); + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe('password'); + }); + + it('should fail when firstName is empty', async () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'test@example.com', + password: 'secret123', + firstName: '', + lastName: 'Doe', + }); + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + const firstNameError = errors.find((e) => e.property === 'firstName'); + expect(firstNameError).toBeDefined(); + }); + + it('should allow empty lastName for single-name users', async () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'cher@example.com', + password: 'secret123', + firstName: 'Cher', + lastName: '', + }); + + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('should allow missing lastName for single-name users', async () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'madonna@example.com', + password: 'secret123', + firstName: 'Madonna', + }); + + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('should transform email to lowercase', () => { + const dto = plainToInstance(AuthRegisterLoginDto, { + email: 'TEST@EXAMPLE.COM', + password: 'secret123', + firstName: 'John', + lastName: 'Doe', + }); + + expect(dto.email).toBe('test@example.com'); + }); + }); +}); diff --git a/src/auth/dto/auth-register-login.dto.ts b/src/auth/dto/auth-register-login.dto.ts index d92a0ee2..90178dae 100644 --- a/src/auth/dto/auth-register-login.dto.ts +++ b/src/auth/dto/auth-register-login.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; import { lowerCaseTransformer } from '../../utils/transformers/lower-case.transformer'; @@ -17,7 +17,7 @@ export class AuthRegisterLoginDto { @IsNotEmpty() firstName: string; - @ApiProperty({ example: 'Doe' }) - @IsNotEmpty() - lastName: string; + @ApiProperty({ example: 'Doe', required: false }) + @IsOptional() + lastName?: string; } diff --git a/src/auth/dto/auth-update.dto.ts b/src/auth/dto/auth-update.dto.ts index b5acc84d..99d589cf 100644 --- a/src/auth/dto/auth-update.dto.ts +++ b/src/auth/dto/auth-update.dto.ts @@ -16,8 +16,7 @@ export class AuthUpdateDto { @ApiPropertyOptional({ example: 'Doe' }) @IsOptional() - @IsNotEmpty({ message: 'Please enter your last name' }) - lastName?: string; + lastName?: string | null; @ApiPropertyOptional({ example: 'new.email@openmeet.net' }) @IsOptional() diff --git a/src/core/constants/constant.ts b/src/core/constants/constant.ts index 3da738d1..7b5753d3 100644 --- a/src/core/constants/constant.ts +++ b/src/core/constants/constant.ts @@ -148,11 +148,12 @@ export enum PostgisSrid { export const DEFAULT_RADIUS = 200; // default radius in Miles for location searching // Slug validation constants -// Slug must be 3-100 characters, start and end with alphanumeric, can contain hyphens +// Slug must be 3-100 characters, start with alphanumeric, can contain hyphens and underscores // Note: Accepts both lower and uppercase - will be normalized to lowercase by the service -export const SLUG_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,98}[a-zA-Z0-9]$/; +// Note: generateShortCode() uses nanoid's urlAlphabet which includes underscores +export const SLUG_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]{1,98}[a-zA-Z0-9_]$/; export const SLUG_VALIDATION_MESSAGE = - 'Slug must be 3-100 characters, alphanumeric with hyphens, and cannot start or end with a hyphen'; + 'Slug must be 3-100 characters, start with alphanumeric, and can contain hyphens or underscores'; export interface TenantConfig { id: string; // tenant id, ex: asdf2jkl diff --git a/src/user/dto/create-user.dto.ts b/src/user/dto/create-user.dto.ts index f594dae0..35b8cda0 100644 --- a/src/user/dto/create-user.dto.ts +++ b/src/user/dto/create-user.dto.ts @@ -33,9 +33,9 @@ export class CreateUserDto { @IsNotEmpty() firstName: string | null; - @ApiProperty({ example: 'Doe', type: String }) - @IsNotEmpty() - lastName: string | null; + @ApiPropertyOptional({ example: 'Doe', type: String }) + @IsOptional() + lastName?: string | null; @ApiPropertyOptional({ type: () => FileDto }) @IsOptional()