mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix: null validation (#25891)
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@ class AssetBulkUpdateDto {
|
|||||||
///
|
///
|
||||||
String? description;
|
String? description;
|
||||||
|
|
||||||
/// Duplicate asset ID
|
/// Duplicate ID
|
||||||
String? duplicateId;
|
String? duplicateId;
|
||||||
|
|
||||||
/// Asset IDs to update
|
/// Asset IDs to update
|
||||||
|
|||||||
+13
-1
@@ -19,6 +19,7 @@ class UserAdminCreateDto {
|
|||||||
required this.name,
|
required this.name,
|
||||||
this.notify,
|
this.notify,
|
||||||
required this.password,
|
required this.password,
|
||||||
|
this.pinCode,
|
||||||
this.quotaSizeInBytes,
|
this.quotaSizeInBytes,
|
||||||
this.shouldChangePassword,
|
this.shouldChangePassword,
|
||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
@@ -54,6 +55,9 @@ class UserAdminCreateDto {
|
|||||||
/// User password
|
/// User password
|
||||||
String password;
|
String password;
|
||||||
|
|
||||||
|
/// PIN code
|
||||||
|
String? pinCode;
|
||||||
|
|
||||||
/// Storage quota in bytes
|
/// Storage quota in bytes
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
@@ -79,6 +83,7 @@ class UserAdminCreateDto {
|
|||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.notify == notify &&
|
other.notify == notify &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
|
other.pinCode == pinCode &&
|
||||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||||
other.shouldChangePassword == shouldChangePassword &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.storageLabel == storageLabel;
|
other.storageLabel == storageLabel;
|
||||||
@@ -92,12 +97,13 @@ class UserAdminCreateDto {
|
|||||||
(name.hashCode) +
|
(name.hashCode) +
|
||||||
(notify == null ? 0 : notify!.hashCode) +
|
(notify == null ? 0 : notify!.hashCode) +
|
||||||
(password.hashCode) +
|
(password.hashCode) +
|
||||||
|
(pinCode == null ? 0 : pinCode!.hashCode) +
|
||||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||||
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
|
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
|
||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, isAdmin=$isAdmin, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, isAdmin=$isAdmin, name=$name, notify=$notify, password=$password, pinCode=$pinCode, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -119,6 +125,11 @@ class UserAdminCreateDto {
|
|||||||
// json[r'notify'] = null;
|
// json[r'notify'] = null;
|
||||||
}
|
}
|
||||||
json[r'password'] = this.password;
|
json[r'password'] = this.password;
|
||||||
|
if (this.pinCode != null) {
|
||||||
|
json[r'pinCode'] = this.pinCode;
|
||||||
|
} else {
|
||||||
|
// json[r'pinCode'] = null;
|
||||||
|
}
|
||||||
if (this.quotaSizeInBytes != null) {
|
if (this.quotaSizeInBytes != null) {
|
||||||
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
|
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
|
||||||
} else {
|
} else {
|
||||||
@@ -152,6 +163,7 @@ class UserAdminCreateDto {
|
|||||||
name: mapValueOfType<String>(json, r'name')!,
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
notify: mapValueOfType<bool>(json, r'notify'),
|
notify: mapValueOfType<bool>(json, r'notify'),
|
||||||
password: mapValueOfType<String>(json, r'password')!,
|
password: mapValueOfType<String>(json, r'password')!,
|
||||||
|
pinCode: mapValueOfType<String>(json, r'pinCode'),
|
||||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
|
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
|
||||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||||
|
|||||||
@@ -15760,7 +15760,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"duplicateId": {
|
"duplicateId": {
|
||||||
"description": "Duplicate asset ID",
|
"description": "Duplicate ID",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -19038,6 +19038,7 @@
|
|||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -19128,6 +19129,7 @@
|
|||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"readAt": {
|
"readAt": {
|
||||||
@@ -25069,6 +25071,12 @@
|
|||||||
"description": "User password",
|
"description": "User password",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"description": "PIN code",
|
||||||
|
"example": "123456",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"quotaSizeInBytes": {
|
"quotaSizeInBytes": {
|
||||||
"description": "Storage quota in bytes",
|
"description": "Storage quota in bytes",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ export type UserAdminCreateDto = {
|
|||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
/** User password */
|
/** User password */
|
||||||
password: string;
|
password: string;
|
||||||
|
/** PIN code */
|
||||||
|
pinCode?: string | null;
|
||||||
/** Storage quota in bytes */
|
/** Storage quota in bytes */
|
||||||
quotaSizeInBytes?: number | null;
|
quotaSizeInBytes?: number | null;
|
||||||
/** Require password change on next login */
|
/** Require password change on next login */
|
||||||
@@ -822,7 +824,7 @@ export type AssetBulkUpdateDto = {
|
|||||||
dateTimeRelative?: number;
|
dateTimeRelative?: number;
|
||||||
/** Asset description */
|
/** Asset description */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** Duplicate asset ID */
|
/** Duplicate ID */
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
/** Asset IDs to update */
|
/** Asset IDs to update */
|
||||||
ids: string[];
|
ids: string[];
|
||||||
|
|||||||
@@ -24,6 +24,34 @@ describe(AssetController.name, () => {
|
|||||||
await request(ctx.getHttpServer()).put(`/assets`);
|
await request(ctx.getHttpServer()).put(`/assets`);
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/assets`)
|
||||||
|
.send({ ids: ['123'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require duplicateId to be a string', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/assets`)
|
||||||
|
.send({ ids: [id], duplicateId: true });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.badRequest(['duplicateId must be a string']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a null duplicateId', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.put(`/assets`)
|
||||||
|
.send({ ids: [id], duplicateId: null });
|
||||||
|
|
||||||
|
expect(service.updateAll).toHaveBeenCalledWith(undefined, expect.objectContaining({ duplicateId: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /assets', () => {
|
describe('DELETE /assets', () => {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
||||||
|
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(NotificationAdminController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(NotificationAdminService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(NotificationAdminController, [
|
||||||
|
{ provide: NotificationAdminService, useValue: service },
|
||||||
|
]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /admin/notifications', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/admin/notifications');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a null readAt', async () => {
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.post(`/admin/notifications`)
|
||||||
|
.send({ title: 'Test', userId: factory.uuid(), readAt: null });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ readAt: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,9 +37,33 @@ describe(NotificationController.name, () => {
|
|||||||
|
|
||||||
describe('PUT /notifications', () => {
|
describe('PUT /notifications', () => {
|
||||||
it('should be an authenticated route', async () => {
|
it('should be an authenticated route', async () => {
|
||||||
await request(ctx.getHttpServer()).get('/notifications');
|
await request(ctx.getHttpServer()).put('/notifications');
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ids', () => {
|
||||||
|
it('should require a list', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['ids must be an array'])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require uuids', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/notifications`)
|
||||||
|
.send({ ids: [true] });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid uuids', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.put(`/notifications`)
|
||||||
|
.send({ ids: [id] });
|
||||||
|
expect(service.updateAll).toHaveBeenCalledWith(undefined, expect.objectContaining({ ids: [id] }));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /notifications/:id', () => {
|
describe('GET /notifications/:id', () => {
|
||||||
@@ -60,5 +84,11 @@ describe(NotificationController.name, () => {
|
|||||||
await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() });
|
await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() });
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should accept a null readAt', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/notifications/${id}`).send({ readAt: null });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ readAt: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ describe(PersonController.name, () => {
|
|||||||
await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' });
|
await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' });
|
||||||
expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null });
|
expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should map an empty color to null', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/people').send({ color: '' });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, { color: null });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /people', () => {
|
describe('DELETE /people', () => {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
|
import { SharedLinkType } from 'src/enum';
|
||||||
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(SharedLinkController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(SharedLinkService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(SharedLinkController, [{ provide: SharedLinkService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /shared-links', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/shared-links');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow an null expiresAt', async () => {
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.post('/shared-links')
|
||||||
|
.send({ expiresAt: null, type: SharedLinkType.Individual });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { TagController } from 'src/controllers/tag.controller';
|
||||||
|
import { TagService } from 'src/services/tag.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { errorDto } from 'test/medium/responses';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(TagController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(TagService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(TagController, [{ provide: TagService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /tags', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get('/tags');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /tags', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/tags');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should a null parentId', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/tags`).send({ name: 'tag', parentId: null });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ parentId: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /tags', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put('/tags');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /tags/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get(`/tags/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /tags/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put(`/tags/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow setting a null color via an empty string', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/tags/${id}`).send({ color: '' });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ color: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,12 +31,55 @@ describe(UserAdminController.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PUT /admin/users/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /admin/users', () => {
|
describe('POST /admin/users', () => {
|
||||||
it('should be an authenticated route', async () => {
|
it('should be an authenticated route', async () => {
|
||||||
await request(ctx.getHttpServer()).post('/admin/users');
|
await request(ctx.getHttpServer()).post('/admin/users');
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow a null pinCode', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/admin/users`).send({
|
||||||
|
name: 'Test user',
|
||||||
|
email: 'test@immich.cloud',
|
||||||
|
password: 'password',
|
||||||
|
pinCode: null,
|
||||||
|
});
|
||||||
|
expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ pinCode: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a null avatarColor', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/admin/users`).send({
|
||||||
|
name: 'Test user',
|
||||||
|
email: 'test@immich.cloud',
|
||||||
|
password: 'password',
|
||||||
|
avatarColor: null,
|
||||||
|
});
|
||||||
|
expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ avatarColor: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should `, async () => {
|
||||||
|
const dto: UserAdminCreateDto = {
|
||||||
|
email: 'user@immich.app',
|
||||||
|
password: 'test',
|
||||||
|
name: 'Test User',
|
||||||
|
quotaSizeInBytes: 1.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post(`/admin/users`)
|
||||||
|
.set('Authorization', `Bearer token`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
||||||
|
});
|
||||||
|
|
||||||
it(`should not allow decimal quota`, async () => {
|
it(`should not allow decimal quota`, async () => {
|
||||||
const dto: UserAdminCreateDto = {
|
const dto: UserAdminCreateDto = {
|
||||||
email: 'user@immich.app',
|
email: 'user@immich.app',
|
||||||
@@ -75,5 +118,17 @@ describe(UserAdminController.name, () => {
|
|||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow a null pinCode', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ pinCode: null });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ pinCode: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a null avatarColor', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ avatarColor: null });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ avatarColor: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ describe(UserController.name, () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('should allow an empty avatarColor', async () => {
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.put(`/users/me`)
|
||||||
|
.set('Authorization', `Bearer token`)
|
||||||
|
.send({ avatarColor: null });
|
||||||
|
expect(service.updateMe).toHaveBeenCalledWith(undefined, expect.objectContaining({ avatarColor: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /users/:id', () => {
|
describe('GET /users/:id', () => {
|
||||||
|
|||||||
@@ -73,8 +73,7 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
|
|||||||
@ValidateUUID({ each: true, description: 'Asset IDs to update' })
|
@ValidateUUID({ each: true, description: 'Asset IDs to update' })
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
|
|
||||||
@ApiProperty({ description: 'Duplicate asset ID' })
|
@ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' })
|
||||||
@Optional()
|
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Relative time offset in seconds' })
|
@ApiProperty({ description: 'Relative time offset in seconds' })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsString } from 'class-validator';
|
import { ArrayMinSize, IsString } from 'class-validator';
|
||||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class TestEmailResponseDto {
|
export class TestEmailResponseDto {
|
||||||
@ApiProperty({ description: 'Email message ID' })
|
@ApiProperty({ description: 'Email message ID' })
|
||||||
@@ -75,20 +75,17 @@ export class NotificationCreateDto {
|
|||||||
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' })
|
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' })
|
||||||
type?: NotificationType;
|
type?: NotificationType;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Notification title' })
|
@ValidateString({ description: 'Notification title' })
|
||||||
@IsString()
|
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Notification description' })
|
@ValidateString({ optional: true, nullable: true, description: 'Notification description' })
|
||||||
@IsString()
|
|
||||||
@Optional({ nullable: true })
|
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Additional notification data' })
|
@ApiPropertyOptional({ description: 'Additional notification data' })
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
data?: any;
|
data?: any;
|
||||||
|
|
||||||
@ValidateDate({ optional: true, description: 'Date when notification was read' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' })
|
||||||
readAt?: Date | null;
|
readAt?: Date | null;
|
||||||
|
|
||||||
@ValidateUUID({ description: 'User ID to send notification to' })
|
@ValidateUUID({ description: 'User ID to send notification to' })
|
||||||
@@ -96,20 +93,22 @@ export class NotificationCreateDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationUpdateDto {
|
export class NotificationUpdateDto {
|
||||||
@ValidateDate({ optional: true, description: 'Date when notification was read' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' })
|
||||||
readAt?: Date | null;
|
readAt?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationUpdateAllDto {
|
export class NotificationUpdateAllDto {
|
||||||
@ValidateUUID({ each: true, optional: true, description: 'Notification IDs to update' })
|
@ValidateUUID({ each: true, description: 'Notification IDs to update' })
|
||||||
|
@ArrayMinSize(1)
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
|
|
||||||
@ValidateDate({ optional: true, description: 'Date when notifications were read' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' })
|
||||||
readAt?: Date | null;
|
readAt?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationDeleteAllDto {
|
export class NotificationDeleteAllDto {
|
||||||
@ValidateUUID({ each: true, description: 'Notification IDs to delete' })
|
@ValidateUUID({ each: true, description: 'Notification IDs to delete' })
|
||||||
|
@ArrayMinSize(1)
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class SharedLinkCreateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
|
|
||||||
@ValidateDate({ optional: true, description: 'Expiration date' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Expiration date' })
|
||||||
expiresAt?: Date | null = null;
|
expiresAt?: Date | null = null;
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true, description: 'Allow uploads' })
|
@ValidateBoolean({ optional: true, description: 'Allow uploads' })
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class TagCreateDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@ValidateUUID({ optional: true, description: 'Parent tag ID' })
|
@ValidateUUID({ nullable: true, optional: true, description: 'Parent tag ID' })
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
||||||
@@ -20,7 +20,7 @@ export class TagCreateDto {
|
|||||||
|
|
||||||
export class TagUpdateDto {
|
export class TagUpdateDto {
|
||||||
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
||||||
@Optional({ emptyToNull: true })
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
@ValidateHexColor()
|
@ValidateHexColor()
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ export class UserUpdateMeDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
|
@ValidateEnum({
|
||||||
|
enum: UserAvatarColor,
|
||||||
|
name: 'UserAvatarColor',
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
description: 'Avatar color',
|
||||||
|
})
|
||||||
avatarColor?: UserAvatarColor | null;
|
avatarColor?: UserAvatarColor | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +102,19 @@ export class UserAdminCreateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
|
@ValidateEnum({
|
||||||
|
enum: UserAvatarColor,
|
||||||
|
name: 'UserAvatarColor',
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
description: 'Avatar color',
|
||||||
|
})
|
||||||
avatarColor?: UserAvatarColor | null;
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'PIN code' })
|
||||||
|
@PinCode({ optional: true, nullable: true, emptyToNull: true })
|
||||||
|
pinCode?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Storage label' })
|
@ApiPropertyOptional({ description: 'Storage label' })
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -135,7 +151,7 @@ export class UserAdminUpdateDto {
|
|||||||
password?: string;
|
password?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'PIN code' })
|
@ApiPropertyOptional({ description: 'PIN code' })
|
||||||
@PinCode({ optional: true, emptyToNull: true })
|
@PinCode({ optional: true, nullable: true, emptyToNull: true })
|
||||||
pinCode?: string | null;
|
pinCode?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'User name' })
|
@ApiPropertyOptional({ description: 'User name' })
|
||||||
@@ -144,7 +160,13 @@ export class UserAdminUpdateDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
|
@ValidateEnum({
|
||||||
|
enum: UserAvatarColor,
|
||||||
|
name: 'UserAvatarColor',
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
description: 'Avatar color',
|
||||||
|
})
|
||||||
avatarColor?: UserAvatarColor | null;
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Storage label' })
|
@ApiPropertyOptional({ description: 'Storage label' })
|
||||||
|
|||||||
+21
-22
@@ -232,19 +232,20 @@ export const ValidateHexColor = () => {
|
|||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
};
|
};
|
||||||
|
|
||||||
type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' };
|
type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' };
|
||||||
export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
||||||
const { optional, nullable, format, ...apiPropertyOptions } = {
|
const {
|
||||||
optional: false,
|
optional,
|
||||||
nullable: false,
|
nullable = false,
|
||||||
format: 'date-time',
|
emptyToNull = false,
|
||||||
...options,
|
format = 'date-time',
|
||||||
};
|
...apiPropertyOptions
|
||||||
|
} = options || {};
|
||||||
|
|
||||||
const decorators = [
|
return applyDecorators(
|
||||||
ApiProperty({ format, ...apiPropertyOptions }),
|
ApiProperty({ format, ...apiPropertyOptions }),
|
||||||
IsDate(),
|
IsDate(),
|
||||||
optional ? Optional({ nullable: true }) : IsNotEmpty(),
|
optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(),
|
||||||
Transform(({ key, value }) => {
|
Transform(({ key, value }) => {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return value;
|
return value;
|
||||||
@@ -256,19 +257,17 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
|||||||
|
|
||||||
return new Date(value as string);
|
return new Date(value as string);
|
||||||
}),
|
}),
|
||||||
];
|
);
|
||||||
|
|
||||||
if (optional) {
|
|
||||||
decorators.push(Optional({ nullable }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyDecorators(...decorators);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StringOptions = { optional?: boolean; nullable?: boolean; trim?: boolean };
|
type StringOptions = OptionalOptions & { optional?: boolean; trim?: boolean };
|
||||||
export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => {
|
export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => {
|
||||||
const { optional, nullable, trim, ...apiPropertyOptions } = options || {};
|
const { optional, nullable, emptyToNull, trim, ...apiPropertyOptions } = options || {};
|
||||||
const decorators = [ApiProperty(apiPropertyOptions), IsString(), optional ? Optional({ nullable }) : IsNotEmpty()];
|
const decorators = [
|
||||||
|
ApiProperty(apiPropertyOptions),
|
||||||
|
IsString(),
|
||||||
|
optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(),
|
||||||
|
];
|
||||||
|
|
||||||
if (trim) {
|
if (trim) {
|
||||||
decorators.push(Transform(({ value }: { value: string }) => value?.trim()));
|
decorators.push(Transform(({ value }: { value: string }) => value?.trim()));
|
||||||
@@ -277,9 +276,9 @@ export const ValidateString = (options?: StringOptions & ApiPropertyOptions) =>
|
|||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
};
|
};
|
||||||
|
|
||||||
type BooleanOptions = { optional?: boolean; nullable?: boolean };
|
type BooleanOptions = OptionalOptions & { optional?: boolean };
|
||||||
export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
|
export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
|
||||||
const { optional, nullable, ...apiPropertyOptions } = options || {};
|
const { optional, nullable, emptyToNull, ...apiPropertyOptions } = options || {};
|
||||||
const decorators = [
|
const decorators = [
|
||||||
Property(apiPropertyOptions),
|
Property(apiPropertyOptions),
|
||||||
IsBoolean(),
|
IsBoolean(),
|
||||||
@@ -291,7 +290,7 @@ export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}),
|
}),
|
||||||
optional ? Optional({ nullable }) : IsNotEmpty(),
|
optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
|
|||||||
Reference in New Issue
Block a user