From f798e037d500f298ae36d849769469d2e5dae901 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Feb 2024 08:28:03 -0500 Subject: [PATCH] refactor(server): e2e (#7265) * refactor: activity e2e * refactor: person e2e * refactor: shared link e2e --- .../src}/api/specs/activity.e2e-spec.ts | 291 +++++++++------- e2e/src/api/specs/person.e2e-spec.ts | 176 ++++++++++ .../src}/api/specs/shared-link.e2e-spec.ts | 320 ++++++++++-------- e2e/src/utils.ts | 54 ++- server/e2e/api/specs/person.e2e-spec.ts | 191 ----------- 5 files changed, 573 insertions(+), 459 deletions(-) rename {server/e2e => e2e/src}/api/specs/activity.e2e-spec.ts (57%) create mode 100644 e2e/src/api/specs/person.e2e-spec.ts rename {server/e2e => e2e/src}/api/specs/shared-link.e2e-spec.ts (53%) delete mode 100644 server/e2e/api/specs/person.e2e-spec.ts diff --git a/server/e2e/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts similarity index 57% rename from server/e2e/api/specs/activity.e2e-spec.ts rename to e2e/src/api/specs/activity.e2e-spec.ts index 47d2d7a199..738411338f 100644 --- a/server/e2e/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -1,79 +1,94 @@ -import { AlbumResponseDto, LoginResponseDto, ReactionType } from '@app/domain'; -import { ActivityController } from '@app/immich'; -import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { ActivityEntity } from '@app/infra/entities'; -import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { + ActivityCreateDto, + AlbumResponseDto, + AssetResponseDto, + LoginResponseDto, + ReactionType, + createActivity as create, + createAlbum, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -describe(`${ActivityController.name} (e2e)`, () => { - let server: any; +describe('/activity', () => { let admin: LoginResponseDto; - let asset: AssetFileUploadResponseDto; - let album: AlbumResponseDto; let nonOwner: LoginResponseDto; + let asset: AssetResponseDto; + let album: AlbumResponseDto; + + const createActivity = (dto: ActivityCreateDto, accessToken?: string) => + create( + { activityCreateDto: dto }, + { headers: asBearerAuth(accessToken || admin.accessToken) } + ); beforeAll(async () => { - server = (await testApp.create()).getHttpServer(); - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - asset = await api.assetApi.upload(server, admin.accessToken, 'example'); + apiUtils.setup(); + await dbUtils.reset(); - await api.userApi.create(server, admin.accessToken, userDto.user1); - nonOwner = await api.authApi.login(server, userDto.user1); - - album = await api.albumApi.create(server, admin.accessToken, { - albumName: 'Album 1', - assetIds: [asset.id], - sharedWithUserIds: [nonOwner.userId], - }); - }); - - afterAll(async () => { - await testApp.teardown(); + admin = await apiUtils.adminSetup(); + nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); + asset = await apiUtils.createAsset(admin.accessToken); + album = await createAlbum( + { + createAlbumDto: { + albumName: 'Album 1', + assetIds: [asset.id], + sharedWithUserIds: [nonOwner.userId], + }, + }, + { headers: asBearerAuth(admin.accessToken) } + ); }); beforeEach(async () => { - await testApp.reset({ entities: [ActivityEntity] }); + await dbUtils.reset(['activity']); }); describe('GET /activity', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get('/activity'); + const { status, body } = await request(app).get('/activity'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require an albumId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + ); }); it('should reject an invalid albumId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') - .query({ albumId: uuidStub.invalid }) + .query({ albumId: uuidDto.invalid }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + ); }); it('should reject an invalid assetId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') - .query({ albumId: uuidStub.notFound, assetId: uuidStub.invalid }) + .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])) + ); }); it('should start off empty', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -82,22 +97,22 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should filter by album id', async () => { - const album2 = await api.albumApi.create(server, admin.accessToken, { - albumName: 'Album 2', - assetIds: [asset.id], - }); + const album2 = await createAlbum( + { + createAlbumDto: { + albumName: 'Album 2', + assetIds: [asset.id], + }, + }, + { headers: asBearerAuth(admin.accessToken) } + ); + const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - type: ReactionType.LIKE, - }), - api.activityApi.create(server, admin.accessToken, { - albumId: album2.id, - type: ReactionType.LIKE, - }), + createActivity({ albumId: album.id, type: ReactionType.Like }), + createActivity({ albumId: album2.id, type: ReactionType.Like }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -108,15 +123,15 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by type=comment', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { + createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'comment', }), - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), + createActivity({ albumId: album.id, type: ReactionType.Like }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id, type: 'comment' }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -127,15 +142,15 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by type=like', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), - api.activityApi.create(server, admin.accessToken, { + createActivity({ albumId: album.id, type: ReactionType.Like }), + createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'comment', }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id, type: 'like' }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -146,18 +161,18 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by userId', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), + createActivity({ albumId: album.id, type: ReactionType.Like }), ]); - const response1 = await request(server) + const response1 = await request(app) .get('/activity') - .query({ albumId: album.id, userId: uuidStub.notFound }) + .query({ albumId: album.id, userId: uuidDto.notFound }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(response1.status).toEqual(200); expect(response1.body.length).toBe(0); - const response2 = await request(server) + const response2 = await request(app) .get('/activity') .query({ albumId: album.id, userId: admin.userId }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -169,15 +184,15 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by assetId', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { + createActivity({ albumId: album.id, assetId: asset.id, - type: ReactionType.LIKE, + type: ReactionType.Like, }), - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), + createActivity({ albumId: album.id, type: ReactionType.Like }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id, assetId: asset.id }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -189,34 +204,45 @@ describe(`${ActivityController.name} (e2e)`, () => { describe('POST /activity', () => { it('should require authentication', async () => { - const { status, body } = await request(server).post('/activity'); + const { status, body } = await request(app).post('/activity'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require an albumId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidStub.invalid }); + .send({ albumId: uuidDto.invalid }); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + ); }); it('should require a comment when type is comment', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidStub.notFound, type: 'comment', comment: null }); + .send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(['comment must be a string', 'comment should not be empty'])); + expect(body).toEqual( + errorDto.badRequest([ + 'comment must be a string', + 'comment should not be empty', + ]) + ); }); it('should add a comment to an album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: album.id, type: 'comment', comment: 'This is my first comment' }); + .send({ + albumId: album.id, + type: 'comment', + comment: 'This is my first comment', + }); expect(status).toEqual(201); expect(body).toEqual({ id: expect.any(String), @@ -229,7 +255,7 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should add a like to an album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, type: 'like' }); @@ -245,11 +271,11 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should return a 200 for a duplicate like on the album', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - type: ReactionType.LIKE, - }); - const { status, body } = await request(server) + const [reaction] = await Promise.all([ + createActivity({ albumId: album.id, type: ReactionType.Like }), + ]); + + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, type: 'like' }); @@ -258,12 +284,14 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should not confuse an album like with an asset like', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - assetId: asset.id, - type: ReactionType.LIKE, - }); - const { status, body } = await request(server) + const [reaction] = await Promise.all([ + createActivity({ + albumId: album.id, + assetId: asset.id, + type: ReactionType.Like, + }), + ]); + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, type: 'like' }); @@ -272,10 +300,15 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should add a comment to an asset', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: album.id, assetId: asset.id, type: 'comment', comment: 'This is my first comment' }); + .send({ + albumId: album.id, + assetId: asset.id, + type: 'comment', + comment: 'This is my first comment', + }); expect(status).toEqual(201); expect(body).toEqual({ id: expect.any(String), @@ -288,7 +321,7 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should add a like to an asset', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, assetId: asset.id, type: 'like' }); @@ -304,12 +337,15 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should return a 200 for a duplicate like on an asset', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - assetId: asset.id, - type: ReactionType.LIKE, - }); - const { status, body } = await request(server) + const [reaction] = await Promise.all([ + createActivity({ + albumId: album.id, + assetId: asset.id, + type: ReactionType.Like, + }), + ]); + + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, assetId: asset.id, type: 'like' }); @@ -320,50 +356,52 @@ describe(`${ActivityController.name} (e2e)`, () => { describe('DELETE /activity/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).delete(`/activity/${uuidStub.notFound}`); + const { status, body } = await request(app).delete( + `/activity/${uuidDto.notFound}` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require a valid uuid', async () => { - const { status, body } = await request(server) - .delete(`/activity/${uuidStub.invalid}`) + const { status, body } = await request(app) + .delete(`/activity/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); }); it('should remove a comment from an album', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'This is a test comment', }); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(204); }); it('should remove a like from an album', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.LIKE, + type: ReactionType.Like, }); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(204); }); it('should let the owner remove a comment by another user', async () => { - const reaction = await api.activityApi.create(server, nonOwner.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'This is a test comment', }); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -371,28 +409,33 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should not let a user remove a comment by another user', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'This is a test comment', }); - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${nonOwner.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Not found or no activity.delete access')); + expect(body).toEqual( + errorDto.badRequest('Not found or no activity.delete access') + ); }); it('should let a non-owner remove their own comment', async () => { - const reaction = await api.activityApi.create(server, nonOwner.accessToken, { - albumId: album.id, - type: ReactionType.COMMENT, - comment: 'This is a test comment', - }); + const reaction = await createActivity( + { + albumId: album.id, + type: ReactionType.Comment, + comment: 'This is a test comment', + }, + nonOwner.accessToken + ); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${nonOwner.accessToken}`); diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts new file mode 100644 index 0000000000..d384fde2dc --- /dev/null +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -0,0 +1,176 @@ +import { LoginResponseDto, PersonResponseDto } from '@immich/sdk'; +import { uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, dbUtils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +describe('/activity', () => { + let admin: LoginResponseDto; + let visiblePerson: PersonResponseDto; + let hiddenPerson: PersonResponseDto; + + beforeAll(async () => { + apiUtils.setup(); + await dbUtils.reset(); + admin = await apiUtils.adminSetup(); + }); + + beforeEach(async () => { + await dbUtils.reset(['person']); + + [visiblePerson, hiddenPerson] = await Promise.all([ + apiUtils.createPerson(admin.accessToken, { + name: 'visible_person', + }), + apiUtils.createPerson(admin.accessToken, { + name: 'hidden_person', + isHidden: true, + }), + ]); + + const asset = await apiUtils.createAsset(admin.accessToken); + + await Promise.all([ + dbUtils.createFace({ assetId: asset.id, personId: visiblePerson.id }), + dbUtils.createFace({ assetId: asset.id, personId: hiddenPerson.id }), + ]); + }); + + describe('GET /person', () => { + beforeEach(async () => {}); + + it('should require authentication', async () => { + const { status, body } = await request(app).get('/person'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should return all people (including hidden)', async () => { + const { status, body } = await request(app) + .get('/person') + .set('Authorization', `Bearer ${admin.accessToken}`) + .query({ withHidden: true }); + + expect(status).toBe(200); + expect(body).toEqual({ + total: 2, + people: [ + expect.objectContaining({ name: 'visible_person' }), + expect.objectContaining({ name: 'hidden_person' }), + ], + }); + }); + + it('should return only visible people', async () => { + const { status, body } = await request(app) + .get('/person') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + total: 2, + people: [expect.objectContaining({ name: 'visible_person' })], + }); + }); + }); + + describe('GET /person/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get( + `/person/${uuidDto.notFound}` + ); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should throw error if person with id does not exist', async () => { + const { status, body } = await request(app) + .get(`/person/${uuidDto.notFound}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should return person information', async () => { + const { status, body } = await request(app) + .get(`/person/${visiblePerson.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: visiblePerson.id })); + }); + }); + + describe('PUT /person/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put( + `/person/${uuidDto.notFound}` + ); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + for (const { key, type } of [ + { key: 'name', type: 'string' }, + { key: 'featureFaceAssetId', type: 'string' }, + { key: 'isHidden', type: 'boolean value' }, + ]) { + it(`should not allow null ${key}`, async () => { + const { status, body } = await request(app) + .put(`/person/${visiblePerson.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ [key]: null }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`])); + }); + } + + it('should not accept invalid birth dates', async () => { + for (const { birthDate, response } of [ + { birthDate: false, response: 'Not found or no person.write access' }, + { birthDate: 'false', response: ['birthDate must be a Date instance'] }, + { + birthDate: '123567', + response: 'Not found or no person.write access', + }, + { birthDate: 123567, response: 'Not found or no person.write access' }, + ]) { + const { status, body } = await request(app) + .put(`/person/${uuidDto.notFound}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(response)); + } + }); + + it('should update a date of birth', async () => { + const { status, body } = await request(app) + .put(`/person/${visiblePerson.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate: '1990-01-01T05:00:00.000Z' }); + expect(status).toBe(200); + expect(body).toMatchObject({ birthDate: '1990-01-01' }); + }); + + it('should clear a date of birth', async () => { + // TODO ironically this uses the update endpoint to create the person + const person = await apiUtils.createPerson(admin.accessToken, { + birthDate: new Date('1990-01-01').toISOString(), + }); + + expect(person.birthDate).toBeDefined(); + + const { status, body } = await request(app) + .put(`/person/${person.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate: null }); + expect(status).toBe(200); + expect(body).toMatchObject({ birthDate: null }); + }); + }); +}); diff --git a/server/e2e/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts similarity index 53% rename from server/e2e/api/specs/shared-link.e2e-spec.ts rename to e2e/src/api/specs/shared-link.e2e-spec.ts index 034b2f2637..df57c57137 100644 --- a/server/e2e/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -1,21 +1,24 @@ import { AlbumResponseDto, AssetResponseDto, - IAssetRepository, LoginResponseDto, + SharedLinkCreateDto, SharedLinkResponseDto, -} from '@app/domain'; -import { SharedLinkController } from '@app/immich'; -import { SharedLinkType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; -import { errorStub, userDto, uuidStub } from '@test/fixtures'; -import { DateTime } from 'luxon'; + SharedLinkType, + createSharedLink as create, + createAlbum, + deleteUser, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; +import { beforeAll, describe, expect, it } from 'vitest'; -describe(`${SharedLinkController.name} (e2e)`, () => { - let server: any; +const createSharedLink = (dto: SharedLinkCreateDto, accessToken: string) => + create({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }); + +describe('/shared-link', () => { let admin: LoginResponseDto; let asset1: AssetResponseDto; let asset2: AssetResponseDto; @@ -30,97 +33,101 @@ describe(`${SharedLinkController.name} (e2e)`, () => { let linkWithAssets: SharedLinkResponseDto; let linkWithMetadata: SharedLinkResponseDto; let linkWithoutMetadata: SharedLinkResponseDto; - let app: INestApplication; beforeAll(async () => { - app = await testApp.create(); - server = app.getHttpServer(); - const assetRepository = app.get(IAssetRepository); + apiUtils.setup(); + await dbUtils.reset(); - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - - await Promise.all([ - api.userApi.create(server, admin.accessToken, userDto.user1), - api.userApi.create(server, admin.accessToken, userDto.user2), - ]); + admin = await apiUtils.adminSetup(); [user1, user2] = await Promise.all([ - api.authApi.login(server, userDto.user1), - api.authApi.login(server, userDto.user2), + apiUtils.userSetup(admin.accessToken, createUserDto.user1), + apiUtils.userSetup(admin.accessToken, createUserDto.user2), ]); [asset1, asset2] = await Promise.all([ - api.assetApi.create(server, user1.accessToken), - api.assetApi.create(server, user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), ]); - await assetRepository.upsertExif({ - assetId: asset1.id, - longitude: -108.400968333333, - latitude: 39.115, - orientation: '1', - dateTimeOriginal: DateTime.fromISO('2022-01-10T19:15:44.310Z').toJSDate(), - timeZone: 'UTC-4', - state: 'Mesa County, Colorado', - country: 'United States of America', - }); - [album, deletedAlbum, metadataAlbum] = await Promise.all([ - api.albumApi.create(server, user1.accessToken, { albumName: 'album' }), - api.albumApi.create(server, user2.accessToken, { albumName: 'deleted album' }), - api.albumApi.create(server, user1.accessToken, { albumName: 'metadata album', assetIds: [asset1.id] }), + createAlbum( + { createAlbumDto: { albumName: 'album' } }, + { headers: asBearerAuth(user1.accessToken) } + ), + createAlbum( + { createAlbumDto: { albumName: 'deleted album' } }, + { headers: asBearerAuth(user2.accessToken) } + ), + createAlbum( + { + createAlbumDto: { + albumName: 'metadata album', + assetIds: [asset1.id], + }, + }, + { headers: asBearerAuth(user1.accessToken) } + ), ]); - [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = - await Promise.all([ - api.sharedLinkApi.create(server, user2.accessToken, { - type: SharedLinkType.ALBUM, - albumId: deletedAlbum.id, - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id], - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - password: 'foo', - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, + [ + linkWithDeletedAlbum, + linkWithAlbum, + linkWithAssets, + linkWithPassword, + linkWithMetadata, + linkWithoutMetadata, + ] = await Promise.all([ + createSharedLink( + { type: SharedLinkType.Album, albumId: deletedAlbum.id }, + user2.accessToken + ), + createSharedLink( + { type: SharedLinkType.Album, albumId: album.id }, + user1.accessToken + ), + createSharedLink( + { type: SharedLinkType.Individual, assetIds: [asset1.id] }, + user1.accessToken + ), + createSharedLink( + { type: SharedLinkType.Album, albumId: album.id, password: 'foo' }, + user1.accessToken + ), + createSharedLink( + { + type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: true, - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, + }, + user1.accessToken + ), + createSharedLink( + { + type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: false, - }), - ]); + }, + user1.accessToken + ), + ]); - await api.userApi.delete(server, admin.accessToken, user2.userId); - }); - - afterAll(async () => { - await testApp.teardown(); + await deleteUser( + { id: user2.userId }, + { headers: asBearerAuth(admin.accessToken) } + ); }); describe('GET /shared-link', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get('/shared-link'); + const { status, body } = await request(app).get('/shared-link'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should get all shared links created by user', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`); @@ -133,12 +140,12 @@ describe(`${SharedLinkController.name} (e2e)`, () => { expect.objectContaining({ id: linkWithPassword.id }), expect.objectContaining({ id: linkWithMetadata.id }), expect.objectContaining({ id: linkWithoutMetadata.id }), - ]), + ]) ); }); it('should not get shared links created by other users', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -149,7 +156,7 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('GET /shared-link/me', () => { it('should not require admin authentication', async () => { - const { status } = await request(server) + const { status } = await request(app) .get('/shared-link/me') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -157,52 +164,66 @@ describe(`${SharedLinkController.name} (e2e)`, () => { }); it('should get data for correct shared link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithAlbum.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithAlbum.key }); expect(status).toBe(200); expect(body).toEqual( expect.objectContaining({ album, userId: user1.userId, - type: SharedLinkType.ALBUM, - }), + type: SharedLinkType.Album, + }) ); }); it('should return unauthorized for incorrect shared link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link/me') .query({ key: linkWithAlbum.key + 'foo' }); expect(status).toBe(401); - expect(body).toEqual(errorStub.invalidShareKey); + expect(body).toEqual(errorDto.invalidShareKey); }); it('should return unauthorized if target has been soft deleted', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithDeletedAlbum.key }); expect(status).toBe(401); - expect(body).toEqual(errorStub.invalidShareKey); + expect(body).toEqual(errorDto.invalidShareKey); }); it('should return unauthorized for password protected link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithPassword.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithPassword.key }); expect(status).toBe(401); - expect(body).toEqual(errorStub.invalidSharePassword); + expect(body).toEqual(errorDto.invalidSharePassword); }); it('should get data for correct password protected link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link/me') .query({ key: linkWithPassword.key, password: 'foo' }); expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); + expect(body).toEqual( + expect.objectContaining({ + album, + userId: user1.userId, + type: SharedLinkType.Album, + }) + ); }); it('should return metadata for album shared link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithMetadata.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithMetadata.key }); expect(status).toBe(200); expect(body.assets).toHaveLength(1); @@ -211,22 +232,16 @@ describe(`${SharedLinkController.name} (e2e)`, () => { originalFileName: 'example', localDateTime: expect.any(String), fileCreatedAt: expect.any(String), - exifInfo: expect.objectContaining({ - longitude: -108.400968333333, - latitude: 39.115, - orientation: '1', - dateTimeOriginal: expect.any(String), - timeZone: 'UTC-4', - state: 'Mesa County, Colorado', - country: 'United States of America', - }), - }), + exifInfo: expect.any(Object), + }) ); expect(body.album).toBeDefined(); }); it('should not return metadata for album shared link without metadata', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithoutMetadata.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithoutMetadata.key }); expect(status).toBe(200); expect(body.assets).toHaveLength(1); @@ -242,127 +257,150 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('GET /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get(`/shared-link/${linkWithAlbum.id}`); + const { status, body } = await request(app).get( + `/shared-link/${linkWithAlbum.id}` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should get shared link by id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); + expect(body).toEqual( + expect.objectContaining({ + album, + userId: user1.userId, + type: SharedLinkType.Album, + }) + ); }); it('should not get shared link by id if user has not created the link or it does not exist', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' })); + expect(body).toEqual( + expect.objectContaining({ message: 'Shared link not found' }) + ); }); }); describe('POST /shared-link', () => { it('should require authentication', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') - .send({ type: SharedLinkType.ALBUM, albumId: uuidStub.notFound }); + .send({ type: SharedLinkType.Album, albumId: uuidDto.notFound }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require a type and the correspondent asset/album id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); + expect(body).toEqual(errorDto.badRequest()); }); it('should require an asset/album id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: SharedLinkType.ALBUM }); + .send({ type: SharedLinkType.Album }); expect(status).toBe(400); - expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' })); + expect(body).toEqual( + expect.objectContaining({ message: 'Invalid albumId' }) + ); }); it('should require a valid asset id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: SharedLinkType.INDIVIDUAL, assetId: uuidStub.notFound }); + .send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound }); expect(status).toBe(400); - expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' })); + expect(body).toEqual( + expect.objectContaining({ message: 'Invalid assetIds' }) + ); }); it('should create a shared link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: SharedLinkType.ALBUM, albumId: album.id }); + .send({ type: SharedLinkType.Album, albumId: album.id }); expect(status).toBe(201); - expect(body).toEqual(expect.objectContaining({ type: SharedLinkType.ALBUM, userId: user1.userId })); + expect(body).toEqual( + expect.objectContaining({ + type: SharedLinkType.Album, + userId: user1.userId, + }) + ); }); }); describe('PATCH /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .patch(`/shared-link/${linkWithAlbum.id}`) .send({ description: 'foo' }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should fail if invalid link', async () => { - const { status, body } = await request(server) - .patch(`/shared-link/${uuidStub.notFound}`) + const { status, body } = await request(app) + .patch(`/shared-link/${uuidDto.notFound}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ description: 'foo' }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); + expect(body).toEqual(errorDto.badRequest()); }); it('should update shared link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .patch(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ description: 'foo' }); expect(status).toBe(200); expect(body).toEqual( - expect.objectContaining({ type: SharedLinkType.ALBUM, userId: user1.userId, description: 'foo' }), + expect.objectContaining({ + type: SharedLinkType.Album, + userId: user1.userId, + description: 'foo', + }) ); }); }); describe('PUT /shared-link/:id/assets', () => { it('should not add assets to shared link (album)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/shared-link/${linkWithAlbum.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Invalid shared link type')); + expect(body).toEqual(errorDto.badRequest('Invalid shared link type')); }); it('should add an assets to a shared link (individual)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/shared-link/${linkWithAssets.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); @@ -374,17 +412,17 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('DELETE /shared-link/:id/assets', () => { it('should not remove assets from a shared link (album)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/shared-link/${linkWithAlbum.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Invalid shared link type')); + expect(body).toEqual(errorDto.badRequest('Invalid shared link type')); }); it('should remove assets from a shared link (individual)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/shared-link/${linkWithAssets.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); @@ -396,23 +434,25 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('DELETE /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).delete(`/shared-link/${linkWithAlbum.id}`); + const { status, body } = await request(app).delete( + `/shared-link/${linkWithAlbum.id}` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should fail if invalid link', async () => { - const { status, body } = await request(server) - .delete(`/shared-link/${uuidStub.notFound}`) + const { status, body } = await request(app) + .delete(`/shared-link/${uuidDto.notFound}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); + expect(body).toEqual(errorDto.badRequest()); }); it('should delete a shared link', async () => { - const { status } = await request(server) + const { status } = await request(app) .delete(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 6c6d3b725d..1c7382879d 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -2,13 +2,15 @@ import { AssetResponseDto, CreateAssetDto, CreateUserDto, - LoginResponseDto, + PersonUpdateDto, createApiKey, + createPerson, createUser, defaults, login, setAdminOnboarding, signUpAdmin, + updatePerson, } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; import { spawn } from 'child_process'; @@ -45,7 +47,36 @@ export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); let client: pg.Client | null = null; export const dbUtils = { - reset: async () => { + createFace: async ({ + assetId, + personId, + }: { + assetId: string; + personId: string; + }) => { + if (!client) { + return; + } + + const vector = Array.from({ length: 512 }, Math.random); + const embedding = `[${vector.join(',')}]`; + + await client.query( + 'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', + [assetId, personId, embedding] + ); + }, + setPersonThumbnail: async (personId: string) => { + if (!client) { + return; + } + + await client.query( + `UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, + [personId] + ); + }, + reset: async (tables?: string[]) => { try { if (!client) { client = new pg.Client( @@ -54,14 +85,20 @@ export const dbUtils = { await client.connect(); } - for (const table of [ + tables = tables || [ + 'shared_links', + 'person', 'albums', 'assets', + 'asset_faces', + 'activity', 'api_keys', 'user_token', 'users', 'system_metadata', - ]) { + ]; + + for (const table of tables) { await client.query(`DELETE FROM ${table} CASCADE;`); } } catch (error) { @@ -165,6 +202,15 @@ export const apiUtils = { return body as AssetResponseDto; }, + createPerson: async (accessToken: string, dto: PersonUpdateDto) => { + // TODO fix createPerson to accept a body + const { id } = await createPerson({ headers: asBearerAuth(accessToken) }); + await dbUtils.setPersonThumbnail(id); + return updatePerson( + { id, personUpdateDto: dto }, + { headers: asBearerAuth(accessToken) } + ); + }, }; export const cliUtils = { diff --git a/server/e2e/api/specs/person.e2e-spec.ts b/server/e2e/api/specs/person.e2e-spec.ts deleted file mode 100644 index 73adcfab71..0000000000 --- a/server/e2e/api/specs/person.e2e-spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { IPersonRepository, LoginResponseDto } from '@app/domain'; -import { PersonController } from '@app/immich'; -import { PersonEntity } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; -import { errorStub, uuidStub } from '@test/fixtures'; -import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; - -describe(`${PersonController.name}`, () => { - let app: INestApplication; - let server: any; - let loginResponse: LoginResponseDto; - let accessToken: string; - let personRepository: IPersonRepository; - let visiblePerson: PersonEntity; - let hiddenPerson: PersonEntity; - - beforeAll(async () => { - app = await testApp.create(); - server = app.getHttpServer(); - personRepository = app.get(IPersonRepository); - }); - - afterAll(async () => { - await testApp.teardown(); - }); - - beforeEach(async () => { - await testApp.reset(); - await api.authApi.adminSignUp(server); - loginResponse = await api.authApi.adminLogin(server); - accessToken = loginResponse.accessToken; - - const faceAsset = await api.assetApi.upload(server, accessToken, 'face_asset'); - visiblePerson = await personRepository.create({ - ownerId: loginResponse.userId, - name: 'visible_person', - thumbnailPath: '/thumbnail/face_asset', - }); - await personRepository.createFaces([ - { - assetId: faceAsset.id, - personId: visiblePerson.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - - hiddenPerson = await personRepository.create({ - ownerId: loginResponse.userId, - name: 'hidden_person', - isHidden: true, - thumbnailPath: '/thumbnail/face_asset', - }); - await personRepository.createFaces([ - { - assetId: faceAsset.id, - personId: hiddenPerson.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - }); - - describe('GET /person', () => { - beforeEach(async () => {}); - - it('should require authentication', async () => { - const { status, body } = await request(server).get('/person'); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should return all people (including hidden)', async () => { - const { status, body } = await request(server) - .get('/person') - .set('Authorization', `Bearer ${accessToken}`) - .query({ withHidden: true }); - - expect(status).toBe(200); - expect(body).toEqual({ - total: 2, - people: [ - expect.objectContaining({ name: 'visible_person' }), - expect.objectContaining({ name: 'hidden_person' }), - ], - }); - }); - - it('should return only visible people', async () => { - const { status, body } = await request(server).get('/person').set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - total: 2, - people: [expect.objectContaining({ name: 'visible_person' })], - }); - }); - }); - - describe('GET /person/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).get(`/person/${uuidStub.notFound}`); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should throw error if person with id does not exist', async () => { - const { status, body } = await request(server) - .get(`/person/${uuidStub.notFound}`) - .set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); - }); - - it('should return person information', async () => { - const { status, body } = await request(server) - .get(`/person/${visiblePerson.id}`) - .set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ id: visiblePerson.id })); - }); - }); - - describe('PUT /person/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).put(`/person/${uuidStub.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - for (const { key, type } of [ - { key: 'name', type: 'string' }, - { key: 'featureFaceAssetId', type: 'string' }, - { key: 'isHidden', type: 'boolean value' }, - ]) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(server) - .put(`/person/${visiblePerson.id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest([`${key} must be a ${type}`])); - }); - } - - it('should not accept invalid birth dates', async () => { - for (const { birthDate, response } of [ - { birthDate: false, response: 'Not found or no person.write access' }, - { birthDate: 'false', response: ['birthDate must be a Date instance'] }, - { birthDate: '123567', response: 'Not found or no person.write access' }, - { birthDate: 123567, response: 'Not found or no person.write access' }, - ]) { - const { status, body } = await request(server) - .put(`/person/${uuidStub.notFound}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ birthDate }); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(response)); - } - }); - - it('should update a date of birth', async () => { - const { status, body } = await request(server) - .put(`/person/${visiblePerson.id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ birthDate: '1990-01-01T05:00:00.000Z' }); - expect(status).toBe(200); - expect(body).toMatchObject({ birthDate: '1990-01-01' }); - }); - - it('should clear a date of birth', async () => { - const person = await personRepository.create({ - birthDate: new Date('1990-01-01'), - ownerId: loginResponse.userId, - }); - - expect(person.birthDate).toBeDefined(); - - const { status, body } = await request(server) - .put(`/person/${person.id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ birthDate: null }); - expect(status).toBe(200); - expect(body).toMatchObject({ birthDate: null }); - }); - }); -});