You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	fix(server): non-nullable IsOptional (#3939)
				
					
				
			* custom `IsOptional` * added link to source * formatting * Update server/src/domain/domain.util.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * nullable birth date endpoint * made `nullable` a property * formatting * removed unused dto * updated decorator arg * fixed album e2e tests * add null tests for auth e2e * add null test for person e2e * fixed tests * added null test for user e2e * removed unusued import * log key in test name * chore: add note about mobile not being able to use the endpoint --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							
								
								
									
										4
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1982,7 +1982,7 @@ export interface PeopleUpdateDto { | ||||
|  */ | ||||
| export interface PeopleUpdateItem { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|      * @type {string} | ||||
|      * @memberof PeopleUpdateItem | ||||
|      */ | ||||
| @@ -2056,7 +2056,7 @@ export interface PersonResponseDto { | ||||
|  */ | ||||
| export interface PersonUpdateDto { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|      * @type {string} | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/PeopleUpdateItem.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/PeopleUpdateItem.md
									
									
									
										generated
									
									
									
								
							| @@ -8,7 +8,7 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **birthDate** | [**DateTime**](DateTime.md) | Person date of birth. | [optional]  | ||||
| **birthDate** | [**DateTime**](DateTime.md) | Person date of birth. Note: the mobile app cannot currently set the birth date to null. | [optional]  | ||||
| **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]  | ||||
| **id** | **String** | Person id. |  | ||||
| **isHidden** | **bool** | Person visibility | [optional]  | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/PersonUpdateDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/PersonUpdateDto.md
									
									
									
										generated
									
									
									
								
							| @@ -8,7 +8,7 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **birthDate** | [**DateTime**](DateTime.md) | Person date of birth. | [optional]  | ||||
| **birthDate** | [**DateTime**](DateTime.md) | Person date of birth. Note: the mobile app cannot currently set the birth date to null. | [optional]  | ||||
| **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]  | ||||
| **isHidden** | **bool** | Person visibility | [optional]  | ||||
| **name** | **String** | Person name. | [optional]  | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/model/people_update_item.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/model/people_update_item.dart
									
									
									
										generated
									
									
									
								
							| @@ -20,7 +20,7 @@ class PeopleUpdateItem { | ||||
|     this.name, | ||||
|   }); | ||||
| 
 | ||||
|   /// Person date of birth. | ||||
|   /// Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|   DateTime? birthDate; | ||||
| 
 | ||||
|   /// Asset is used to get the feature face thumbnail. | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/model/person_update_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/model/person_update_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -19,7 +19,7 @@ class PersonUpdateDto { | ||||
|     this.name, | ||||
|   }); | ||||
| 
 | ||||
|   /// Person date of birth. | ||||
|   /// Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|   DateTime? birthDate; | ||||
| 
 | ||||
|   /// Asset is used to get the feature face thumbnail. | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/test/people_update_item_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/people_update_item_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,7 +16,7 @@ void main() { | ||||
|   // final instance = PeopleUpdateItem(); | ||||
| 
 | ||||
|   group('test PeopleUpdateItem', () { | ||||
|     // Person date of birth. | ||||
|     // Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|     // DateTime birthDate | ||||
|     test('to test the property `birthDate`', () async { | ||||
|       // TODO | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/test/person_update_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/person_update_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,7 +16,7 @@ void main() { | ||||
|   // final instance = PersonUpdateDto(); | ||||
| 
 | ||||
|   group('test PersonUpdateDto', () { | ||||
|     // Person date of birth. | ||||
|     // Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|     // DateTime birthDate | ||||
|     test('to test the property `birthDate`', () async { | ||||
|       // TODO | ||||
|   | ||||
| @@ -6331,7 +6331,7 @@ | ||||
|       "PeopleUpdateItem": { | ||||
|         "properties": { | ||||
|           "birthDate": { | ||||
|             "description": "Person date of birth.", | ||||
|             "description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.", | ||||
|             "format": "date", | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
| @@ -6390,7 +6390,7 @@ | ||||
|       "PersonUpdateDto": { | ||||
|         "properties": { | ||||
|           "birthDate": { | ||||
|             "description": "Person date of birth.", | ||||
|             "description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.", | ||||
|             "format": "date", | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { ValidateUUID } from '../../domain.util'; | ||||
| import { IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { Optional, ValidateUUID } from '../../domain.util'; | ||||
|  | ||||
| export class CreateAlbumDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -9,7 +9,7 @@ export class CreateAlbumDto { | ||||
|   albumName!: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   description?: string; | ||||
|  | ||||
|   @ValidateUUID({ optional: true, each: true }) | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { IsOptional, IsString } from 'class-validator'; | ||||
| import { ValidateUUID } from '../../domain.util'; | ||||
| import { IsString } from 'class-validator'; | ||||
| import { ValidateUUID, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class UpdateAlbumDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   albumName?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   description?: string; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { toBoolean } from '../../domain.util'; | ||||
| import { IsBoolean } from 'class-validator'; | ||||
| import { toBoolean, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class AlbumInfoDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   withoutAssets?: boolean; | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID } from '../../domain.util'; | ||||
| import { IsBoolean } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class GetAlbumsDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   @ApiProperty() | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
|  | ||||
| import { IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { Optional } from '../domain.util'; | ||||
| export class APIKeyCreateDto { | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   name?: string; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| import { AssetType } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { toBoolean } from '../../domain.util'; | ||||
| import { IsBoolean } from 'class-validator'; | ||||
| import { toBoolean, Optional } from '../../domain.util'; | ||||
| import { AssetStats } from '../asset.repository'; | ||||
|  | ||||
| export class AssetStatsDto { | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   isFavorite?: boolean; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { IsBoolean } from 'class-validator'; | ||||
| import { BulkIdsDto } from '../response-dto'; | ||||
| import { Optional } from '../../domain.util'; | ||||
|  | ||||
| export class AssetBulkUpdateDto extends BulkIdsDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isArchived?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsInt, IsOptional, IsPositive } from 'class-validator'; | ||||
| import { ValidateUUID } from '../../domain.util'; | ||||
| import { IsInt, IsPositive } from 'class-validator'; | ||||
| import { Optional, ValidateUUID } from '../../domain.util'; | ||||
|  | ||||
| export class DownloadInfoDto { | ||||
|   @ValidateUUID({ each: true, optional: true }) | ||||
| @@ -14,7 +14,7 @@ export class DownloadInfoDto { | ||||
|  | ||||
|   @IsInt() | ||||
|   @IsPositive() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @ApiProperty({ type: 'integer' }) | ||||
|   archiveSize?: number; | ||||
| } | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform, Type } from 'class-transformer'; | ||||
| import { IsBoolean, IsDate, IsOptional } from 'class-validator'; | ||||
| import { toBoolean } from '../../domain.util'; | ||||
| import { IsBoolean, IsDate } from 'class-validator'; | ||||
| import { toBoolean, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class MapMarkerDto { | ||||
|   @ApiProperty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   fileCreatedAfter?: Date; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   fileCreatedBefore?: Date; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID } from '../../domain.util'; | ||||
| import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID, Optional } from '../../domain.util'; | ||||
| import { TimeBucketSize } from '../asset.repository'; | ||||
|  | ||||
| export class TimeBucketDto { | ||||
| @@ -19,12 +19,12 @@ export class TimeBucketDto { | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   personId?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   isFavorite?: boolean; | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { EntityType } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { IsDate, IsEnum, IsOptional, IsUUID } from 'class-validator'; | ||||
| import { IsDate, IsEnum, IsUUID } from 'class-validator'; | ||||
| import { Optional } from '../domain.util'; | ||||
|  | ||||
| export class AuditDeletesDto { | ||||
|   @IsDate() | ||||
| @@ -12,7 +13,7 @@ export class AuditDeletesDto { | ||||
|   @IsEnum(EntityType) | ||||
|   entityType!: EntityType; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsUUID('4') | ||||
|   @ApiProperty({ format: 'uuid' }) | ||||
|   userId?: string; | ||||
|   | ||||
| @@ -4,8 +4,9 @@ import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; | ||||
|  | ||||
| export class LoginCredentialDto { | ||||
|   @IsEmail({ require_tld: false }) | ||||
|   @Transform(({ value }) => value?.toLowerCase()) | ||||
|   @IsNotEmpty() | ||||
|   @ApiProperty({ example: 'testuser@email.com' }) | ||||
|   @Transform(({ value }) => value.toLowerCase()) | ||||
|   email!: string; | ||||
|  | ||||
|   @IsString() | ||||
|   | ||||
| @@ -4,8 +4,9 @@ import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; | ||||
|  | ||||
| export class SignUpDto { | ||||
|   @IsEmail({ require_tld: false }) | ||||
|   @Transform(({ value }) => value?.toLowerCase()) | ||||
|   @IsNotEmpty() | ||||
|   @ApiProperty({ example: 'testuser@email.com' }) | ||||
|   @Transform(({ value }) => value.toLowerCase()) | ||||
|   email!: string; | ||||
|  | ||||
|   @IsString() | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { applyDecorators } from '@nestjs/common'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; | ||||
| import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID, ValidationOptions, ValidateIf } from 'class-validator'; | ||||
| import { basename, extname } from 'node:path'; | ||||
| import sanitize from 'sanitize-filename'; | ||||
|  | ||||
| @@ -13,7 +13,7 @@ export function ValidateUUID({ optional, each }: Options = { optional: false, ea | ||||
|   return applyDecorators( | ||||
|     IsUUID('4', { each }), | ||||
|     ApiProperty({ format: 'uuid' }), | ||||
|     optional ? IsOptional() : IsNotEmpty(), | ||||
|     optional ? Optional() : IsNotEmpty(), | ||||
|     each ? IsArray() : IsString(), | ||||
|   ); | ||||
| } | ||||
| @@ -92,3 +92,23 @@ export async function* usePagination<T>( | ||||
|     yield result.items; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface OptionalOptions extends ValidationOptions { | ||||
|   nullable?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Checks if value is missing and if so, ignores all validators. | ||||
|  * | ||||
|  * @param validationOptions {@link OptionalOptions} | ||||
|  * | ||||
|  * @see IsOptional exported from `class-validator. | ||||
|  */ | ||||
| // https://stackoverflow.com/a/71353929 | ||||
| export function Optional({ nullable, ...validationOptions }: OptionalOptions = {}) { | ||||
|   if (nullable === true) { | ||||
|     return IsOptional(validationOptions); | ||||
|   } | ||||
|  | ||||
|   return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsBoolean, IsEnum, IsNotEmpty, IsOptional } from 'class-validator'; | ||||
| import { IsBoolean, IsEnum, IsNotEmpty } from 'class-validator'; | ||||
| import { JobCommand, QueueName } from './job.constants'; | ||||
| import { Optional } from '../domain.util'; | ||||
|  | ||||
| export class JobIdParamDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -15,7 +16,7 @@ export class JobCommandDto { | ||||
|   @ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' }) | ||||
|   command!: JobCommand; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   force!: boolean; | ||||
| } | ||||
|   | ||||
| @@ -1,47 +1,38 @@ | ||||
| import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform, Type } from 'class-transformer'; | ||||
| import { | ||||
|   IsArray, | ||||
|   IsBoolean, | ||||
|   IsDate, | ||||
|   IsNotEmpty, | ||||
|   IsOptional, | ||||
|   IsString, | ||||
|   ValidateIf, | ||||
|   ValidateNested, | ||||
| } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID } from '../domain.util'; | ||||
| import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; | ||||
| import { Optional, toBoolean, ValidateUUID } from '../domain.util'; | ||||
|  | ||||
| export class PersonUpdateDto { | ||||
|   /** | ||||
|    * Person name. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   name?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person date of birth. | ||||
|    * Note: the mobile app cannot currently set the birth date to null. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @Optional({ nullable: true }) | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   @ValidateIf((value) => value !== null) | ||||
|   @ApiProperty({ format: 'date' }) | ||||
|   birthDate?: Date | null; | ||||
|  | ||||
|   /** | ||||
|    * Asset is used to get the feature face thumbnail. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   featureFaceAssetId?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person visibility | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isHidden?: boolean; | ||||
| } | ||||
| @@ -53,43 +44,13 @@ export class PeopleUpdateDto { | ||||
|   people!: PeopleUpdateItem[]; | ||||
| } | ||||
|  | ||||
| export class PeopleUpdateItem { | ||||
| export class PeopleUpdateItem extends PersonUpdateDto { | ||||
|   /** | ||||
|    * Person id. | ||||
|    */ | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   id!: string; | ||||
|  | ||||
|   /** | ||||
|    * Person name. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsString() | ||||
|   name?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person date of birth. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   @ApiProperty({ format: 'date' }) | ||||
|   birthDate?: Date | null; | ||||
|  | ||||
|   /** | ||||
|    * Asset is used to get the feature face thumbnail. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsString() | ||||
|   featureFaceAssetId?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person visibility | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   isHidden?: boolean; | ||||
| } | ||||
|  | ||||
| export class MergePersonDto { | ||||
|   | ||||
| @@ -1,87 +1,87 @@ | ||||
| import { AssetType } from '@app/infra/entities'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { toBoolean } from '../../domain.util'; | ||||
| import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { toBoolean, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class SearchDto { | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   q?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   query?: string; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(toBoolean) | ||||
|   clip?: boolean; | ||||
|  | ||||
|   @IsEnum(AssetType) | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   type?: AssetType; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(toBoolean) | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(toBoolean) | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   'exifInfo.city'?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   'exifInfo.state'?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   'exifInfo.country'?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   'exifInfo.make'?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   'exifInfo.model'?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   'exifInfo.projectionType'?: string; | ||||
|  | ||||
|   @IsString({ each: true }) | ||||
|   @IsArray() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(({ value }) => value.split(',')) | ||||
|   'smartInfo.objects'?: string[]; | ||||
|  | ||||
|   @IsString({ each: true }) | ||||
|   @IsArray() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(({ value }) => value.split(',')) | ||||
|   'smartInfo.tags'?: string[]; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(toBoolean) | ||||
|   recent?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(toBoolean) | ||||
|   motion?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { SharedLinkType } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator'; | ||||
| import { ValidateUUID } from '../domain.util'; | ||||
| import { IsBoolean, IsDate, IsEnum, IsString } from 'class-validator'; | ||||
| import { ValidateUUID, Optional } from '../domain.util'; | ||||
|  | ||||
| export class SharedLinkCreateDto { | ||||
|   @IsEnum(SharedLinkType) | ||||
| @@ -16,40 +16,40 @@ export class SharedLinkCreateDto { | ||||
|   albumId?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   description?: string; | ||||
|  | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   @IsOptional() | ||||
|   @Optional({ nullable: true }) | ||||
|   expiresAt?: Date | null = null; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   allowUpload?: boolean = false; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   allowDownload?: boolean = true; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   showExif?: boolean = true; | ||||
| } | ||||
|  | ||||
| export class SharedLinkEditDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   description?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional({ nullable: true }) | ||||
|   expiresAt?: Date | null; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   allowUpload?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   allowDownload?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   showExif?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; | ||||
| import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; | ||||
| import { CLIPMode, ModelType } from '../machine-learning.interface'; | ||||
| import { Optional } from '../../domain.util'; | ||||
|  | ||||
| export class ModelConfig { | ||||
|   @IsBoolean() | ||||
| @@ -12,7 +13,7 @@ export class ModelConfig { | ||||
|   modelName!: string; | ||||
|  | ||||
|   @IsEnum(ModelType) | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @ApiProperty({ enumName: 'ModelType', enum: ModelType }) | ||||
|   modelType?: ModelType; | ||||
| } | ||||
| @@ -28,7 +29,7 @@ export class ClassificationConfig extends ModelConfig { | ||||
|  | ||||
| export class CLIPConfig extends ModelConfig { | ||||
|   @IsEnum(CLIPMode) | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @ApiProperty({ enumName: 'CLIPMode', enum: CLIPMode }) | ||||
|   mode?: CLIPMode; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { TagType } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { Optional } from '../domain.util'; | ||||
|  | ||||
| export class CreateTagDto { | ||||
|   @IsString() | ||||
| @@ -15,6 +16,6 @@ export class CreateTagDto { | ||||
|  | ||||
| export class UpdateTagDto { | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   name?: string; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { toEmail, toSanitized } from '../../domain.util'; | ||||
| import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { toEmail, toSanitized, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class CreateUserDto { | ||||
|   @IsEmail({ require_tld: false }) | ||||
| @@ -19,16 +19,16 @@ export class CreateUserDto { | ||||
|   @IsString() | ||||
|   lastName!: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional({ nullable: true }) | ||||
|   @IsString() | ||||
|   @Transform(toSanitized) | ||||
|   storageLabel?: string | null; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional({ nullable: true }) | ||||
|   @IsString() | ||||
|   externalPath?: string | null; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   memoriesEnabled?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -1,35 +1,35 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; | ||||
| import { toEmail, toSanitized } from '../../domain.util'; | ||||
| import { IsBoolean, IsEmail, IsNotEmpty, IsString, IsUUID } from 'class-validator'; | ||||
| import { toEmail, toSanitized, Optional } from '../../domain.util'; | ||||
|  | ||||
| export class UpdateUserDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsEmail({ require_tld: false }) | ||||
|   @Transform(toEmail) | ||||
|   email?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsNotEmpty() | ||||
|   @IsString() | ||||
|   password?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   firstName?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   lastName?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   @Transform(toSanitized) | ||||
|   storageLabel?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   externalPath?: string; | ||||
|  | ||||
| @@ -38,15 +38,15 @@ export class UpdateUserDto { | ||||
|   @ApiProperty({ format: 'uuid' }) | ||||
|   id!: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isAdmin?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   shouldChangePassword?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   memoriesEnabled?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import { Optional } from '../../domain.util'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { IsBoolean } from 'class-validator'; | ||||
|  | ||||
| export class UserCountDto { | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(({ value }) => value === 'true') | ||||
|   /** | ||||
|    * When true, return the number of admins accounts | ||||
|   | ||||
| @@ -1,31 +1,31 @@ | ||||
| import { toBoolean } from '@app/domain'; | ||||
| import { Optional, toBoolean } from '@app/domain'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform, Type } from 'class-transformer'; | ||||
| import { IsBoolean, IsDate, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator'; | ||||
| import { IsBoolean, IsDate, IsNotEmpty, IsNumber, IsUUID } from 'class-validator'; | ||||
|  | ||||
| export class AssetSearchDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsNotEmpty() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsNotEmpty() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsNumber() | ||||
|   skip?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsUUID('4') | ||||
|   @ApiProperty({ format: 'uuid' }) | ||||
|   userId?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   updatedAfter?: Date; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { toBoolean, toSanitized, UploadFieldName } from '@app/domain'; | ||||
| import { Optional, toBoolean, toSanitized, UploadFieldName } from '@app/domain'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; | ||||
|  | ||||
| export class CreateAssetBase { | ||||
|   @IsNotEmpty() | ||||
| @@ -19,20 +19,20 @@ export class CreateAssetBase { | ||||
|   @IsNotEmpty() | ||||
|   isFavorite!: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isVisible?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   duration?: string; | ||||
| } | ||||
|  | ||||
| export class CreateAssetDto extends CreateAssetBase { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   isReadOnly?: boolean = false; | ||||
| @@ -50,7 +50,7 @@ export class CreateAssetDto extends CreateAssetBase { | ||||
| } | ||||
|  | ||||
| export class ImportAssetDto extends CreateAssetBase { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @Transform(toBoolean) | ||||
|   isReadOnly?: boolean = true; | ||||
|  | ||||
| @@ -60,7 +60,7 @@ export class ImportAssetDto extends CreateAssetBase { | ||||
|   assetPath!: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsNotEmpty() | ||||
|   @Transform(toSanitized) | ||||
|   sidecarPath?: string; | ||||
|   | ||||
| @@ -1,45 +0,0 @@ | ||||
| import { IsNotEmpty, IsOptional } from 'class-validator'; | ||||
|  | ||||
| export class CreateExifDto { | ||||
|   @IsNotEmpty() | ||||
|   assetId!: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   make?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   model?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   exifImageWidth?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   exifImageHeight?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   fileSizeInByte?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   orientation?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   dateTimeOriginal?: Date; | ||||
|  | ||||
|   @IsOptional() | ||||
|   modifiedDate?: Date; | ||||
|  | ||||
|   @IsOptional() | ||||
|   lensModel?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   fNumber?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   focalLenght?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   iso?: number; | ||||
|  | ||||
|   @IsOptional() | ||||
|   exposureTime?: number; | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Optional } from '@app/domain'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsEnum, IsOptional } from 'class-validator'; | ||||
| import { IsEnum } from 'class-validator'; | ||||
|  | ||||
| export enum GetAssetThumbnailFormatEnum { | ||||
|   JPEG = 'JPEG', | ||||
| @@ -7,7 +8,7 @@ export enum GetAssetThumbnailFormatEnum { | ||||
| } | ||||
|  | ||||
| export class GetAssetThumbnailDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsEnum(GetAssetThumbnailFormatEnum) | ||||
|   @ApiProperty({ | ||||
|     type: String, | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| import { toBoolean } from '@app/domain'; | ||||
| import { Optional, toBoolean } from '@app/domain'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { IsBoolean } from 'class-validator'; | ||||
|  | ||||
| export class ServeFileDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   @ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' }) | ||||
|   isThumb?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   @ApiProperty({ type: Boolean, title: 'Is request made from web' }) | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| import { Optional } from '@app/domain'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { IsArray, IsBoolean, IsNotEmpty, IsString } from 'class-validator'; | ||||
|  | ||||
| export class UpdateAssetDto { | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsArray() | ||||
|   @IsString({ each: true }) | ||||
|   @IsNotEmpty({ each: true }) | ||||
| @@ -26,7 +27,7 @@ export class UpdateAssetDto { | ||||
|   }) | ||||
|   tagIds?: string[]; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @Optional() | ||||
|   @IsString() | ||||
|   description?: string; | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,16 @@ import { AppModule, AuthController } from '@app/immich'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { deviceStub, errorStub, loginResponseStub, signupResponseStub, signupStub, uuidStub } from '../fixtures'; | ||||
| import { | ||||
|   changePasswordStub, | ||||
|   deviceStub, | ||||
|   errorStub, | ||||
|   loginResponseStub, | ||||
|   loginStub, | ||||
|   signupResponseStub, | ||||
|   adminSignupStub, | ||||
|   uuidStub, | ||||
| } from '../fixtures'; | ||||
| import { api, db } from '../test-utils'; | ||||
|  | ||||
| const firstName = 'Immich'; | ||||
| @@ -64,7 +73,7 @@ describe(`${AuthController.name} (e2e)`, () => { | ||||
|     it('should sign up the admin with a local domain', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post('/auth/admin-sign-up') | ||||
|         .send({ ...signupStub, email: 'admin@local' }); | ||||
|         .send({ ...adminSignupStub, email: 'admin@local' }); | ||||
|       expect(status).toEqual(201); | ||||
|       expect(body).toEqual({ ...signupResponseStub, email: 'admin@local' }); | ||||
|     }); | ||||
| @@ -72,7 +81,7 @@ describe(`${AuthController.name} (e2e)`, () => { | ||||
|     it('should transform email to lower case', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post('/auth/admin-sign-up') | ||||
|         .send({ ...signupStub, email: 'aDmIn@IMMICH.app' }); | ||||
|         .send({ ...adminSignupStub, email: 'aDmIn@IMMICH.app' }); | ||||
|       expect(status).toEqual(201); | ||||
|       expect(body).toEqual(signupResponseStub); | ||||
|     }); | ||||
| @@ -80,11 +89,21 @@ describe(`${AuthController.name} (e2e)`, () => { | ||||
|     it('should not allow a second admin to sign up', async () => { | ||||
|       await api.adminSignUp(server); | ||||
|  | ||||
|       const { status, body } = await request(server).post('/auth/admin-sign-up').send(signupStub); | ||||
|       const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub); | ||||
|  | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorStub.alreadyHasAdmin); | ||||
|     }); | ||||
|  | ||||
|     for (const key of Object.keys(adminSignupStub)) { | ||||
|       it(`should not allow null ${key}`, async () => { | ||||
|         const { status, body } = await request(server) | ||||
|           .post('/auth/admin-sign-up') | ||||
|           .send({ ...adminSignupStub, [key]: null }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   describe(`POST /auth/login`, () => { | ||||
| @@ -94,6 +113,16 @@ describe(`${AuthController.name} (e2e)`, () => { | ||||
|       expect(status).toBe(401); | ||||
|     }); | ||||
|  | ||||
|     for (const key of Object.keys(loginStub.admin)) { | ||||
|       it(`should not allow null ${key}`, async () => { | ||||
|         const { status, body } = await request(server) | ||||
|           .post('/auth/login') | ||||
|           .send({ ...loginStub.admin, [key]: null }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     it('should accept a correct password', async () => { | ||||
|       const { status, body, headers } = await request(server).post('/auth/login').send({ email, password }); | ||||
|       expect(status).toBe(201); | ||||
| @@ -183,17 +212,26 @@ describe(`${AuthController.name} (e2e)`, () => { | ||||
|  | ||||
|   describe('POST /auth/change-password', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/auth/change-password`) | ||||
|         .send({ password: 'Password123', newPassword: 'Password1234' }); | ||||
|       const { status, body } = await request(server).post(`/auth/change-password`).send(changePasswordStub); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     for (const key of Object.keys(changePasswordStub)) { | ||||
|       it(`should not allow null ${key}`, async () => { | ||||
|         const { status, body } = await request(server) | ||||
|           .post('/auth/change-password') | ||||
|           .send({ ...changePasswordStub, [key]: null }) | ||||
|           .set('Authorization', `Bearer ${accessToken}`); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     it('should require the current password', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/auth/change-password`) | ||||
|         .send({ password: 'wrong-password', newPassword: 'Password1234' }) | ||||
|         .send({ ...changePasswordStub, password: 'wrong-password' }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorStub.wrongPassword); | ||||
| @@ -202,7 +240,7 @@ describe(`${AuthController.name} (e2e)`, () => { | ||||
|     it('should change the password', async () => { | ||||
|       const { status } = await request(server) | ||||
|         .post(`/auth/change-password`) | ||||
|         .send({ password: 'Password123', newPassword: 'Password1234' }) | ||||
|         .send(changePasswordStub) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|  | ||||
|   | ||||
| @@ -40,7 +40,20 @@ describe(`${PersonController.name}`, () => { | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should not accept invalid dates', async () => { | ||||
|     for (const key of ['name', 'featureFaceAssetId', 'isHidden']) { | ||||
|       it(`should not allow null ${key}`, async () => { | ||||
|         const personRepository = app.get<IPersonRepository>(IPersonRepository); | ||||
|         const person = await personRepository.create({ ownerId: loginResponse.userId }); | ||||
|         const { status, body } = await request(server) | ||||
|           .put(`/person/${person.id}`) | ||||
|           .set('Authorization', `Bearer ${accessToken}`) | ||||
|           .send({ [key]: null }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     it('should not accept invalid birth dates', async () => { | ||||
|       for (const birthDate of [false, 'false', '123567', 123456]) { | ||||
|         const { status, body } = await request(server) | ||||
|           .put(`/person/${uuidStub.notFound}`) | ||||
| @@ -50,6 +63,7 @@ describe(`${PersonController.name}`, () => { | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     it('should update a date of birth', async () => { | ||||
|       const personRepository = app.get<IPersonRepository>(IPersonRepository); | ||||
|       const person = await personRepository.create({ ownerId: loginResponse.userId }); | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { AppModule, UserController } from '@app/immich'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { errorStub } from '../fixtures'; | ||||
| import { errorStub, userSignupStub, userStub } from '../fixtures'; | ||||
| import { api, db } from '../test-utils'; | ||||
|  | ||||
| describe(`${UserController.name}`, () => { | ||||
| @@ -118,13 +118,22 @@ describe(`${UserController.name}`, () => { | ||||
|  | ||||
|   describe('POST /user', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/user`) | ||||
|         .send({ email: 'user1@immich.app', password: 'Password123', firstName: 'Immich', lastName: 'User' }); | ||||
|       const { status, body } = await request(server).post(`/user`).send(userSignupStub); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     for (const key of Object.keys(userSignupStub)) { | ||||
|       it(`should not allow null ${key}`, async () => { | ||||
|         const { status, body } = await request(server) | ||||
|           .post(`/user`) | ||||
|           .set('Authorization', `Bearer ${accessToken}`) | ||||
|           .send({ ...userSignupStub, [key]: null }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     it('should ignore `isAdmin`', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/user`) | ||||
| @@ -170,6 +179,17 @@ describe(`${UserController.name}`, () => { | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     for (const key of Object.keys(userStub.admin)) { | ||||
|       it(`should not allow null ${key}`, async () => { | ||||
|         const { status, body } = await request(server) | ||||
|           .put(`/user`) | ||||
|           .set('Authorization', `Bearer ${accessToken}`) | ||||
|           .send({ ...userStub.admin, [key]: null }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     it('should not allow a non-admin to become an admin', async () => { | ||||
|       const user = await api.userApi.create(server, accessToken, { | ||||
|         email: 'user1@immich.app', | ||||
|   | ||||
							
								
								
									
										12
									
								
								server/test/fixtures/auth.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								server/test/fixtures/auth.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +1,17 @@ | ||||
| import { AuthUserDto } from '@app/domain'; | ||||
|  | ||||
| export const signupStub = { | ||||
| export const adminSignupStub = { | ||||
|   firstName: 'Immich', | ||||
|   lastName: 'Admin', | ||||
|   email: 'admin@immich.app', | ||||
|   password: 'Password123', | ||||
| }; | ||||
|  | ||||
| export const userSignupStub = { | ||||
|   ...adminSignupStub, | ||||
|   memoriesEnabled: true, | ||||
| }; | ||||
|  | ||||
| export const signupResponseStub = { | ||||
|   id: expect.any(String), | ||||
|   email: 'admin@immich.app', | ||||
| @@ -22,6 +27,11 @@ export const loginStub = { | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const changePasswordStub = { | ||||
|   password: 'Password123', | ||||
|   newPassword: 'Password1234', | ||||
| }; | ||||
|  | ||||
| export const authStub = { | ||||
|   admin: Object.freeze<AuthUserDto>({ | ||||
|     id: 'admin_id', | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import { | ||||
| } from '@app/domain'; | ||||
| import { dataSource } from '@app/infra'; | ||||
| import request from 'supertest'; | ||||
| import { loginResponseStub, loginStub, signupResponseStub, signupStub } from './fixtures'; | ||||
| import { loginResponseStub, loginStub, signupResponseStub, adminSignupStub } from './fixtures'; | ||||
|  | ||||
| export const db = { | ||||
|   reset: async () => { | ||||
| @@ -49,7 +49,7 @@ export function getAuthUser(): AuthUserDto { | ||||
|  | ||||
| export const api = { | ||||
|   adminSignUp: async (server: any) => { | ||||
|     const { status, body } = await request(server).post('/auth/admin-sign-up').send(signupStub); | ||||
|     const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub); | ||||
|  | ||||
|     expect(status).toBe(201); | ||||
|     expect(body).toEqual(signupResponseStub); | ||||
|   | ||||
							
								
								
									
										4
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1982,7 +1982,7 @@ export interface PeopleUpdateDto { | ||||
|  */ | ||||
| export interface PeopleUpdateItem { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|      * @type {string} | ||||
|      * @memberof PeopleUpdateItem | ||||
|      */ | ||||
| @@ -2056,7 +2056,7 @@ export interface PersonResponseDto { | ||||
|  */ | ||||
| export interface PersonUpdateDto { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * Person date of birth. Note: the mobile app cannot currently set the birth date to null. | ||||
|      * @type {string} | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user