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:
Mert 2023-09-01 12:40:00 -04:00 committed by GitHub
parent ca35e5557b
commit 9539a361e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 271 additions and 243 deletions

View File

@ -1982,7 +1982,7 @@ export interface PeopleUpdateDto {
*/ */
export interface PeopleUpdateItem { 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} * @type {string}
* @memberof PeopleUpdateItem * @memberof PeopleUpdateItem
*/ */
@ -2056,7 +2056,7 @@ export interface PersonResponseDto {
*/ */
export interface PersonUpdateDto { 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} * @type {string}
* @memberof PersonUpdateDto * @memberof PersonUpdateDto
*/ */

View File

@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes 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] **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]
**id** | **String** | Person id. | **id** | **String** | Person id. |
**isHidden** | **bool** | Person visibility | [optional] **isHidden** | **bool** | Person visibility | [optional]

View File

@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes 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] **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]
**isHidden** | **bool** | Person visibility | [optional] **isHidden** | **bool** | Person visibility | [optional]
**name** | **String** | Person name. | [optional] **name** | **String** | Person name. | [optional]

View File

@ -20,7 +20,7 @@ class PeopleUpdateItem {
this.name, this.name,
}); });
/// Person date of birth. /// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate; DateTime? birthDate;
/// Asset is used to get the feature face thumbnail. /// Asset is used to get the feature face thumbnail.

View File

@ -19,7 +19,7 @@ class PersonUpdateDto {
this.name, this.name,
}); });
/// Person date of birth. /// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate; DateTime? birthDate;
/// Asset is used to get the feature face thumbnail. /// Asset is used to get the feature face thumbnail.

View File

@ -16,7 +16,7 @@ void main() {
// final instance = PeopleUpdateItem(); // final instance = PeopleUpdateItem();
group('test 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 // DateTime birthDate
test('to test the property `birthDate`', () async { test('to test the property `birthDate`', () async {
// TODO // TODO

View File

@ -16,7 +16,7 @@ void main() {
// final instance = PersonUpdateDto(); // final instance = PersonUpdateDto();
group('test 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 // DateTime birthDate
test('to test the property `birthDate`', () async { test('to test the property `birthDate`', () async {
// TODO // TODO

View File

@ -6331,7 +6331,7 @@
"PeopleUpdateItem": { "PeopleUpdateItem": {
"properties": { "properties": {
"birthDate": { "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", "format": "date",
"nullable": true, "nullable": true,
"type": "string" "type": "string"
@ -6390,7 +6390,7 @@
"PersonUpdateDto": { "PersonUpdateDto": {
"properties": { "properties": {
"birthDate": { "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", "format": "date",
"nullable": true, "nullable": true,
"type": "string" "type": "string"

View File

@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util'; import { Optional, ValidateUUID } from '../../domain.util';
export class CreateAlbumDto { export class CreateAlbumDto {
@IsNotEmpty() @IsNotEmpty()
@ -9,7 +9,7 @@ export class CreateAlbumDto {
albumName!: string; albumName!: string;
@IsString() @IsString()
@IsOptional() @Optional()
description?: string; description?: string;
@ValidateUUID({ optional: true, each: true }) @ValidateUUID({ optional: true, each: true })

View File

@ -1,12 +1,12 @@
import { IsOptional, IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util'; import { ValidateUUID, Optional } from '../../domain.util';
export class UpdateAlbumDto { export class UpdateAlbumDto {
@IsOptional() @Optional()
@IsString() @IsString()
albumName?: string; albumName?: string;
@IsOptional() @Optional()
@IsString() @IsString()
description?: string; description?: string;

View File

@ -1,9 +1,9 @@
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean } from 'class-validator';
import { toBoolean } from '../../domain.util'; import { toBoolean, Optional } from '../../domain.util';
export class AlbumInfoDto { export class AlbumInfoDto {
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
withoutAssets?: boolean; withoutAssets?: boolean;

View File

@ -1,10 +1,10 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean } from 'class-validator';
import { toBoolean, ValidateUUID } from '../../domain.util'; import { toBoolean, ValidateUUID, Optional } from '../../domain.util';
export class GetAlbumsDto { export class GetAlbumsDto {
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
@ApiProperty() @ApiProperty()

View File

@ -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 { export class APIKeyCreateDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
name?: string; name?: string;
} }

View File

@ -1,19 +1,19 @@
import { AssetType } from '@app/infra/entities'; import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean } from 'class-validator';
import { toBoolean } from '../../domain.util'; import { toBoolean, Optional } from '../../domain.util';
import { AssetStats } from '../asset.repository'; import { AssetStats } from '../asset.repository';
export class AssetStatsDto { export class AssetStatsDto {
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
@IsOptional() @Optional()
isArchived?: boolean; isArchived?: boolean;
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
@IsOptional() @Optional()
isFavorite?: boolean; isFavorite?: boolean;
} }

View File

@ -1,12 +1,13 @@
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean } from 'class-validator';
import { BulkIdsDto } from '../response-dto'; import { BulkIdsDto } from '../response-dto';
import { Optional } from '../../domain.util';
export class AssetBulkUpdateDto extends BulkIdsDto { export class AssetBulkUpdateDto extends BulkIdsDto {
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isFavorite?: boolean; isFavorite?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isArchived?: boolean; isArchived?: boolean;
} }

View File

@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsOptional, IsPositive } from 'class-validator'; import { IsInt, IsPositive } from 'class-validator';
import { ValidateUUID } from '../../domain.util'; import { Optional, ValidateUUID } from '../../domain.util';
export class DownloadInfoDto { export class DownloadInfoDto {
@ValidateUUID({ each: true, optional: true }) @ValidateUUID({ each: true, optional: true })
@ -14,7 +14,7 @@ export class DownloadInfoDto {
@IsInt() @IsInt()
@IsPositive() @IsPositive()
@IsOptional() @Optional()
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
archiveSize?: number; archiveSize?: number;
} }

View File

@ -1,21 +1,21 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsDate, IsOptional } from 'class-validator'; import { IsBoolean, IsDate } from 'class-validator';
import { toBoolean } from '../../domain.util'; import { toBoolean, Optional } from '../../domain.util';
export class MapMarkerDto { export class MapMarkerDto {
@ApiProperty() @ApiProperty()
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@IsOptional() @Optional()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
fileCreatedAfter?: Date; fileCreatedAfter?: Date;
@IsOptional() @Optional()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
fileCreatedBefore?: Date; fileCreatedBefore?: Date;

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { toBoolean, ValidateUUID } from '../../domain.util'; import { toBoolean, ValidateUUID, Optional } from '../../domain.util';
import { TimeBucketSize } from '../asset.repository'; import { TimeBucketSize } from '../asset.repository';
export class TimeBucketDto { export class TimeBucketDto {
@ -19,12 +19,12 @@ export class TimeBucketDto {
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
personId?: string; personId?: string;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;

View File

@ -1,7 +1,8 @@
import { EntityType } from '@app/infra/entities'; import { EntityType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; 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 { export class AuditDeletesDto {
@IsDate() @IsDate()
@ -12,7 +13,7 @@ export class AuditDeletesDto {
@IsEnum(EntityType) @IsEnum(EntityType)
entityType!: EntityType; entityType!: EntityType;
@IsOptional() @Optional()
@IsUUID('4') @IsUUID('4')
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
userId?: string; userId?: string;

View File

@ -4,8 +4,9 @@ import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class LoginCredentialDto { export class LoginCredentialDto {
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value.toLowerCase())
email!: string; email!: string;
@IsString() @IsString()

View File

@ -4,8 +4,9 @@ import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class SignUpDto { export class SignUpDto {
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value.toLowerCase())
email!: string; email!: string;
@IsString() @IsString()

View File

@ -1,6 +1,6 @@
import { applyDecorators } from '@nestjs/common'; import { applyDecorators } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger'; 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 { basename, extname } from 'node:path';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
@ -13,7 +13,7 @@ export function ValidateUUID({ optional, each }: Options = { optional: false, ea
return applyDecorators( return applyDecorators(
IsUUID('4', { each }), IsUUID('4', { each }),
ApiProperty({ format: 'uuid' }), ApiProperty({ format: 'uuid' }),
optional ? IsOptional() : IsNotEmpty(), optional ? Optional() : IsNotEmpty(),
each ? IsArray() : IsString(), each ? IsArray() : IsString(),
); );
} }
@ -92,3 +92,23 @@ export async function* usePagination<T>(
yield result.items; 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);
}

View File

@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { JobCommand, QueueName } from './job.constants';
import { Optional } from '../domain.util';
export class JobIdParamDto { export class JobIdParamDto {
@IsNotEmpty() @IsNotEmpty()
@ -15,7 +16,7 @@ export class JobCommandDto {
@ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' }) @ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' })
command!: JobCommand; command!: JobCommand;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
force!: boolean; force!: boolean;
} }

View File

@ -1,47 +1,38 @@
import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
IsArray, import { Optional, toBoolean, ValidateUUID } from '../domain.util';
IsBoolean,
IsDate,
IsNotEmpty,
IsOptional,
IsString,
ValidateIf,
ValidateNested,
} from 'class-validator';
import { toBoolean, ValidateUUID } from '../domain.util';
export class PersonUpdateDto { export class PersonUpdateDto {
/** /**
* Person name. * Person name.
*/ */
@IsOptional() @Optional()
@IsString() @IsString()
name?: string; name?: string;
/** /**
* Person date of birth. * Person date of birth.
* Note: the mobile app cannot currently set the birth date to null.
*/ */
@IsOptional() @Optional({ nullable: true })
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
@ValidateIf((value) => value !== null)
@ApiProperty({ format: 'date' }) @ApiProperty({ format: 'date' })
birthDate?: Date | null; birthDate?: Date | null;
/** /**
* Asset is used to get the feature face thumbnail. * Asset is used to get the feature face thumbnail.
*/ */
@IsOptional() @Optional()
@IsString() @IsString()
featureFaceAssetId?: string; featureFaceAssetId?: string;
/** /**
* Person visibility * Person visibility
*/ */
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isHidden?: boolean; isHidden?: boolean;
} }
@ -53,43 +44,13 @@ export class PeopleUpdateDto {
people!: PeopleUpdateItem[]; people!: PeopleUpdateItem[];
} }
export class PeopleUpdateItem { export class PeopleUpdateItem extends PersonUpdateDto {
/** /**
* Person id. * Person id.
*/ */
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
id!: string; 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 { export class MergePersonDto {

View File

@ -1,87 +1,87 @@
import { AssetType } from '@app/infra/entities'; import { AssetType } from '@app/infra/entities';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { toBoolean } from '../../domain.util'; import { toBoolean, Optional } from '../../domain.util';
export class SearchDto { export class SearchDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
q?: string; q?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
query?: string; query?: string;
@IsBoolean() @IsBoolean()
@IsOptional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
clip?: boolean; clip?: boolean;
@IsEnum(AssetType) @IsEnum(AssetType)
@IsOptional() @Optional()
type?: AssetType; type?: AssetType;
@IsBoolean() @IsBoolean()
@IsOptional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@IsBoolean() @IsBoolean()
@IsOptional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
'exifInfo.city'?: string; 'exifInfo.city'?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
'exifInfo.state'?: string; 'exifInfo.state'?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
'exifInfo.country'?: string; 'exifInfo.country'?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
'exifInfo.make'?: string; 'exifInfo.make'?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
'exifInfo.model'?: string; 'exifInfo.model'?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @Optional()
'exifInfo.projectionType'?: string; 'exifInfo.projectionType'?: string;
@IsString({ each: true }) @IsString({ each: true })
@IsArray() @IsArray()
@IsOptional() @Optional()
@Transform(({ value }) => value.split(',')) @Transform(({ value }) => value.split(','))
'smartInfo.objects'?: string[]; 'smartInfo.objects'?: string[];
@IsString({ each: true }) @IsString({ each: true })
@IsArray() @IsArray()
@IsOptional() @Optional()
@Transform(({ value }) => value.split(',')) @Transform(({ value }) => value.split(','))
'smartInfo.tags'?: string[]; 'smartInfo.tags'?: string[];
@IsBoolean() @IsBoolean()
@IsOptional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
recent?: boolean; recent?: boolean;
@IsBoolean() @IsBoolean()
@IsOptional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
motion?: boolean; motion?: boolean;
} }

View File

@ -1,8 +1,8 @@
import { SharedLinkType } from '@app/infra/entities'; import { SharedLinkType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator'; import { IsBoolean, IsDate, IsEnum, IsString } from 'class-validator';
import { ValidateUUID } from '../domain.util'; import { ValidateUUID, Optional } from '../domain.util';
export class SharedLinkCreateDto { export class SharedLinkCreateDto {
@IsEnum(SharedLinkType) @IsEnum(SharedLinkType)
@ -16,40 +16,40 @@ export class SharedLinkCreateDto {
albumId?: string; albumId?: string;
@IsString() @IsString()
@IsOptional() @Optional()
description?: string; description?: string;
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
@IsOptional() @Optional({ nullable: true })
expiresAt?: Date | null = null; expiresAt?: Date | null = null;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
allowUpload?: boolean = false; allowUpload?: boolean = false;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
allowDownload?: boolean = true; allowDownload?: boolean = true;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
showExif?: boolean = true; showExif?: boolean = true;
} }
export class SharedLinkEditDto { export class SharedLinkEditDto {
@IsOptional() @Optional()
description?: string; description?: string;
@IsOptional() @Optional({ nullable: true })
expiresAt?: Date | null; expiresAt?: Date | null;
@IsOptional() @Optional()
allowUpload?: boolean; allowUpload?: boolean;
@IsOptional() @Optional()
allowDownload?: boolean; allowDownload?: boolean;
@IsOptional() @Optional()
showExif?: boolean; showExif?: boolean;
} }

View File

@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; 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 { CLIPMode, ModelType } from '../machine-learning.interface';
import { Optional } from '../../domain.util';
export class ModelConfig { export class ModelConfig {
@IsBoolean() @IsBoolean()
@ -12,7 +13,7 @@ export class ModelConfig {
modelName!: string; modelName!: string;
@IsEnum(ModelType) @IsEnum(ModelType)
@IsOptional() @Optional()
@ApiProperty({ enumName: 'ModelType', enum: ModelType }) @ApiProperty({ enumName: 'ModelType', enum: ModelType })
modelType?: ModelType; modelType?: ModelType;
} }
@ -28,7 +29,7 @@ export class ClassificationConfig extends ModelConfig {
export class CLIPConfig extends ModelConfig { export class CLIPConfig extends ModelConfig {
@IsEnum(CLIPMode) @IsEnum(CLIPMode)
@IsOptional() @Optional()
@ApiProperty({ enumName: 'CLIPMode', enum: CLIPMode }) @ApiProperty({ enumName: 'CLIPMode', enum: CLIPMode })
mode?: CLIPMode; mode?: CLIPMode;
} }

View File

@ -1,6 +1,7 @@
import { TagType } from '@app/infra/entities'; import { TagType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; 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 { export class CreateTagDto {
@IsString() @IsString()
@ -15,6 +16,6 @@ export class CreateTagDto {
export class UpdateTagDto { export class UpdateTagDto {
@IsString() @IsString()
@IsOptional() @Optional()
name?: string; name?: string;
} }

View File

@ -1,6 +1,6 @@
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { toEmail, toSanitized } from '../../domain.util'; import { toEmail, toSanitized, Optional } from '../../domain.util';
export class CreateUserDto { export class CreateUserDto {
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@ -19,16 +19,16 @@ export class CreateUserDto {
@IsString() @IsString()
lastName!: string; lastName!: string;
@IsOptional() @Optional({ nullable: true })
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)
storageLabel?: string | null; storageLabel?: string | null;
@IsOptional() @Optional({ nullable: true })
@IsString() @IsString()
externalPath?: string | null; externalPath?: string | null;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
} }

View File

@ -1,35 +1,35 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; import { IsBoolean, IsEmail, IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { toEmail, toSanitized } from '../../domain.util'; import { toEmail, toSanitized, Optional } from '../../domain.util';
export class UpdateUserDto { export class UpdateUserDto {
@IsOptional() @Optional()
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@Transform(toEmail) @Transform(toEmail)
email?: string; email?: string;
@IsOptional() @Optional()
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
password?: string; password?: string;
@IsOptional() @Optional()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
firstName?: string; firstName?: string;
@IsOptional() @Optional()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
lastName?: string; lastName?: string;
@IsOptional() @Optional()
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)
storageLabel?: string; storageLabel?: string;
@IsOptional() @Optional()
@IsString() @IsString()
externalPath?: string; externalPath?: string;
@ -38,15 +38,15 @@ export class UpdateUserDto {
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
id!: string; id!: string;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isAdmin?: boolean; isAdmin?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
} }

View File

@ -1,9 +1,10 @@
import { Optional } from '../../domain.util';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean } from 'class-validator';
export class UserCountDto { export class UserCountDto {
@IsBoolean() @IsBoolean()
@IsOptional() @Optional()
@Transform(({ value }) => value === 'true') @Transform(({ value }) => value === 'true')
/** /**
* When true, return the number of admins accounts * When true, return the number of admins accounts

View File

@ -1,31 +1,31 @@
import { toBoolean } from '@app/domain'; import { Optional, toBoolean } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; 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 { export class AssetSearchDto {
@IsOptional() @Optional()
@IsNotEmpty() @IsNotEmpty()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@IsOptional() @Optional()
@IsNotEmpty() @IsNotEmpty()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@IsOptional() @Optional()
@IsNumber() @IsNumber()
skip?: number; skip?: number;
@IsOptional() @Optional()
@IsUUID('4') @IsUUID('4')
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
userId?: string; userId?: string;
@IsOptional() @Optional()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
updatedAfter?: Date; updatedAfter?: Date;

View File

@ -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 { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
export class CreateAssetBase { export class CreateAssetBase {
@IsNotEmpty() @IsNotEmpty()
@ -19,20 +19,20 @@ export class CreateAssetBase {
@IsNotEmpty() @IsNotEmpty()
isFavorite!: boolean; isFavorite!: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isArchived?: boolean; isArchived?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isVisible?: boolean; isVisible?: boolean;
@IsOptional() @Optional()
duration?: string; duration?: string;
} }
export class CreateAssetDto extends CreateAssetBase { export class CreateAssetDto extends CreateAssetBase {
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
isReadOnly?: boolean = false; isReadOnly?: boolean = false;
@ -50,7 +50,7 @@ export class CreateAssetDto extends CreateAssetBase {
} }
export class ImportAssetDto extends CreateAssetBase { export class ImportAssetDto extends CreateAssetBase {
@IsOptional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
isReadOnly?: boolean = true; isReadOnly?: boolean = true;
@ -60,7 +60,7 @@ export class ImportAssetDto extends CreateAssetBase {
assetPath!: string; assetPath!: string;
@IsString() @IsString()
@IsOptional() @Optional()
@IsNotEmpty() @IsNotEmpty()
@Transform(toSanitized) @Transform(toSanitized)
sidecarPath?: string; sidecarPath?: string;

View File

@ -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;
}

View File

@ -1,5 +1,6 @@
import { Optional } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator'; import { IsEnum } from 'class-validator';
export enum GetAssetThumbnailFormatEnum { export enum GetAssetThumbnailFormatEnum {
JPEG = 'JPEG', JPEG = 'JPEG',
@ -7,7 +8,7 @@ export enum GetAssetThumbnailFormatEnum {
} }
export class GetAssetThumbnailDto { export class GetAssetThumbnailDto {
@IsOptional() @Optional()
@IsEnum(GetAssetThumbnailFormatEnum) @IsEnum(GetAssetThumbnailFormatEnum)
@ApiProperty({ @ApiProperty({
type: String, type: String,

View File

@ -1,16 +1,16 @@
import { toBoolean } from '@app/domain'; import { Optional, toBoolean } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean } from 'class-validator';
export class ServeFileDto { export class ServeFileDto {
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
@ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' }) @ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' })
isThumb?: boolean; isThumb?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
@ApiProperty({ type: Boolean, title: 'Is request made from web' }) @ApiProperty({ type: Boolean, title: 'Is request made from web' })

View File

@ -1,16 +1,17 @@
import { Optional } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; 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 { export class UpdateAssetDto {
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isFavorite?: boolean; isFavorite?: boolean;
@IsOptional() @Optional()
@IsBoolean() @IsBoolean()
isArchived?: boolean; isArchived?: boolean;
@IsOptional() @Optional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
@IsNotEmpty({ each: true }) @IsNotEmpty({ each: true })
@ -26,7 +27,7 @@ export class UpdateAssetDto {
}) })
tagIds?: string[]; tagIds?: string[];
@IsOptional() @Optional()
@IsString() @IsString()
description?: string; description?: string;
} }

View File

@ -2,7 +2,16 @@ import { AppModule, AuthController } from '@app/immich';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest'; 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'; import { api, db } from '../test-utils';
const firstName = 'Immich'; const firstName = 'Immich';
@ -64,7 +73,7 @@ describe(`${AuthController.name} (e2e)`, () => {
it('should sign up the admin with a local domain', async () => { it('should sign up the admin with a local domain', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ ...signupStub, email: 'admin@local' }); .send({ ...adminSignupStub, email: 'admin@local' });
expect(status).toEqual(201); expect(status).toEqual(201);
expect(body).toEqual({ ...signupResponseStub, email: 'admin@local' }); expect(body).toEqual({ ...signupResponseStub, email: 'admin@local' });
}); });
@ -72,7 +81,7 @@ describe(`${AuthController.name} (e2e)`, () => {
it('should transform email to lower case', async () => { it('should transform email to lower case', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ ...signupStub, email: 'aDmIn@IMMICH.app' }); .send({ ...adminSignupStub, email: 'aDmIn@IMMICH.app' });
expect(status).toEqual(201); expect(status).toEqual(201);
expect(body).toEqual(signupResponseStub); expect(body).toEqual(signupResponseStub);
}); });
@ -80,11 +89,21 @@ describe(`${AuthController.name} (e2e)`, () => {
it('should not allow a second admin to sign up', async () => { it('should not allow a second admin to sign up', async () => {
await api.adminSignUp(server); 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(status).toBe(400);
expect(body).toEqual(errorStub.alreadyHasAdmin); 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`, () => { describe(`POST /auth/login`, () => {
@ -94,6 +113,16 @@ describe(`${AuthController.name} (e2e)`, () => {
expect(status).toBe(401); 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 () => { it('should accept a correct password', async () => {
const { status, body, headers } = await request(server).post('/auth/login').send({ email, password }); const { status, body, headers } = await request(server).post('/auth/login').send({ email, password });
expect(status).toBe(201); expect(status).toBe(201);
@ -183,17 +212,26 @@ describe(`${AuthController.name} (e2e)`, () => {
describe('POST /auth/change-password', () => { describe('POST /auth/change-password', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(server).post(`/auth/change-password`).send(changePasswordStub);
.post(`/auth/change-password`)
.send({ password: 'Password123', newPassword: 'Password1234' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); 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 () => { it('should require the current password', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
.post(`/auth/change-password`) .post(`/auth/change-password`)
.send({ password: 'wrong-password', newPassword: 'Password1234' }) .send({ ...changePasswordStub, password: 'wrong-password' })
.set('Authorization', `Bearer ${accessToken}`); .set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorStub.wrongPassword); expect(body).toEqual(errorStub.wrongPassword);
@ -202,7 +240,7 @@ describe(`${AuthController.name} (e2e)`, () => {
it('should change the password', async () => { it('should change the password', async () => {
const { status } = await request(server) const { status } = await request(server)
.post(`/auth/change-password`) .post(`/auth/change-password`)
.send({ password: 'Password123', newPassword: 'Password1234' }) .send(changePasswordStub)
.set('Authorization', `Bearer ${accessToken}`); .set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);

View File

@ -40,7 +40,20 @@ describe(`${PersonController.name}`, () => {
expect(body).toEqual(errorStub.unauthorized); 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]) { for (const birthDate of [false, 'false', '123567', 123456]) {
const { status, body } = await request(server) const { status, body } = await request(server)
.put(`/person/${uuidStub.notFound}`) .put(`/person/${uuidStub.notFound}`)
@ -50,6 +63,7 @@ describe(`${PersonController.name}`, () => {
expect(body).toEqual(errorStub.badRequest); expect(body).toEqual(errorStub.badRequest);
} }
}); });
it('should update a date of birth', async () => { it('should update a date of birth', async () => {
const personRepository = app.get<IPersonRepository>(IPersonRepository); const personRepository = app.get<IPersonRepository>(IPersonRepository);
const person = await personRepository.create({ ownerId: loginResponse.userId }); const person = await personRepository.create({ ownerId: loginResponse.userId });

View File

@ -3,7 +3,7 @@ import { AppModule, UserController } from '@app/immich';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest'; import request from 'supertest';
import { errorStub } from '../fixtures'; import { errorStub, userSignupStub, userStub } from '../fixtures';
import { api, db } from '../test-utils'; import { api, db } from '../test-utils';
describe(`${UserController.name}`, () => { describe(`${UserController.name}`, () => {
@ -118,13 +118,22 @@ describe(`${UserController.name}`, () => {
describe('POST /user', () => { describe('POST /user', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(server).post(`/user`).send(userSignupStub);
.post(`/user`)
.send({ email: 'user1@immich.app', password: 'Password123', firstName: 'Immich', lastName: 'User' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); 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 () => { it('should ignore `isAdmin`', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
.post(`/user`) .post(`/user`)
@ -170,6 +179,17 @@ describe(`${UserController.name}`, () => {
expect(body).toEqual(errorStub.unauthorized); 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 () => { it('should not allow a non-admin to become an admin', async () => {
const user = await api.userApi.create(server, accessToken, { const user = await api.userApi.create(server, accessToken, {
email: 'user1@immich.app', email: 'user1@immich.app',

View File

@ -1,12 +1,17 @@
import { AuthUserDto } from '@app/domain'; import { AuthUserDto } from '@app/domain';
export const signupStub = { export const adminSignupStub = {
firstName: 'Immich', firstName: 'Immich',
lastName: 'Admin', lastName: 'Admin',
email: 'admin@immich.app', email: 'admin@immich.app',
password: 'Password123', password: 'Password123',
}; };
export const userSignupStub = {
...adminSignupStub,
memoriesEnabled: true,
};
export const signupResponseStub = { export const signupResponseStub = {
id: expect.any(String), id: expect.any(String),
email: 'admin@immich.app', email: 'admin@immich.app',
@ -22,6 +27,11 @@ export const loginStub = {
}, },
}; };
export const changePasswordStub = {
password: 'Password123',
newPassword: 'Password1234',
};
export const authStub = { export const authStub = {
admin: Object.freeze<AuthUserDto>({ admin: Object.freeze<AuthUserDto>({
id: 'admin_id', id: 'admin_id',

View File

@ -14,7 +14,7 @@ import {
} from '@app/domain'; } from '@app/domain';
import { dataSource } from '@app/infra'; import { dataSource } from '@app/infra';
import request from 'supertest'; import request from 'supertest';
import { loginResponseStub, loginStub, signupResponseStub, signupStub } from './fixtures'; import { loginResponseStub, loginStub, signupResponseStub, adminSignupStub } from './fixtures';
export const db = { export const db = {
reset: async () => { reset: async () => {
@ -49,7 +49,7 @@ export function getAuthUser(): AuthUserDto {
export const api = { export const api = {
adminSignUp: async (server: any) => { 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(status).toBe(201);
expect(body).toEqual(signupResponseStub); expect(body).toEqual(signupResponseStub);

View File

@ -1982,7 +1982,7 @@ export interface PeopleUpdateDto {
*/ */
export interface PeopleUpdateItem { 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} * @type {string}
* @memberof PeopleUpdateItem * @memberof PeopleUpdateItem
*/ */
@ -2056,7 +2056,7 @@ export interface PersonResponseDto {
*/ */
export interface PersonUpdateDto { 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} * @type {string}
* @memberof PersonUpdateDto * @memberof PersonUpdateDto
*/ */