From 6c6a32c63e371d9cd3f5220aa6a4f7d94224af77 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 26 Jun 2025 19:52:10 -0400 Subject: [PATCH] refactor: memory medium tests (#19568) --- e2e/src/api/specs/memory.e2e-spec.ts | 203 +----------------- .../src/controllers/memory.controller.spec.ts | 132 ++++++++++++ .../specs/services/memory.service.spec.ts | 73 ++++++- 3 files changed, 206 insertions(+), 202 deletions(-) create mode 100644 server/src/controllers/memory.controller.spec.ts diff --git a/e2e/src/api/specs/memory.e2e-spec.ts b/e2e/src/api/specs/memory.e2e-spec.ts index d91a570f77..e5e2351738 100644 --- a/e2e/src/api/specs/memory.e2e-spec.ts +++ b/e2e/src/api/specs/memory.e2e-spec.ts @@ -6,7 +6,7 @@ import { createMemory, getMemory, } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -17,7 +17,6 @@ describe('/memories', () => { let user: LoginResponseDto; let adminAsset: AssetMediaResponseDto; let userAsset1: AssetMediaResponseDto; - let userAsset2: AssetMediaResponseDto; let userMemory: MemoryResponseDto; beforeAll(async () => { @@ -25,10 +24,9 @@ describe('/memories', () => { admin = await utils.adminSetup(); user = await utils.userSetup(admin.accessToken, createUserDto.user1); - [adminAsset, userAsset1, userAsset2] = await Promise.all([ + [adminAsset, userAsset1] = await Promise.all([ utils.createAsset(admin.accessToken), utils.createAsset(user.accessToken), - utils.createAsset(user.accessToken), ]); userMemory = await createMemory( { @@ -43,121 +41,7 @@ describe('/memories', () => { ); }); - describe('GET /memories', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/memories'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('POST /memories', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/memories'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should validate data when type is on this day', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: {}, - memoryAt: new Date(2021).toISOString(), - }); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), - ); - }); - - it('should create a new memory', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: { year: 2021 }, - memoryAt: new Date(2021).toISOString(), - }); - - expect(status).toBe(201); - expect(body).toEqual({ - id: expect.any(String), - type: 'on_this_day', - data: { year: 2021 }, - createdAt: expect.any(String), - updatedAt: expect.any(String), - isSaved: false, - memoryAt: expect.any(String), - ownerId: user.userId, - assets: [], - }); - }); - - it('should create a new memory (with assets)', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: { year: 2021 }, - memoryAt: new Date(2021).toISOString(), - assetIds: [userAsset1.id, userAsset2.id], - }); - - expect(status).toBe(201); - expect(body).toMatchObject({ - id: expect.any(String), - assets: expect.arrayContaining([ - expect.objectContaining({ id: userAsset1.id }), - expect.objectContaining({ id: userAsset2.id }), - ]), - }); - expect(body.assets).toHaveLength(2); - }); - - it('should create a new memory and ignore assets the user does not have access to', async () => { - const { status, body } = await request(app) - .post('/memories') - .set('Authorization', `Bearer ${user.accessToken}`) - .send({ - type: 'on_this_day', - data: { year: 2021 }, - memoryAt: new Date(2021).toISOString(), - assetIds: [userAsset1.id, adminAsset.id], - }); - - expect(status).toBe(201); - expect(body).toMatchObject({ - id: expect.any(String), - assets: [expect.objectContaining({ id: userAsset1.id })], - }); - expect(body.assets).toHaveLength(1); - }); - }); - describe('GET /memories/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .get(`/memories/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .get(`/memories/${userMemory.id}`) @@ -176,22 +60,6 @@ describe('/memories', () => { }); describe('PUT /memories/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .put(`/memories/${uuidDto.invalid}`) - .send({ isSaved: true }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .put(`/memories/${userMemory.id}`) @@ -218,23 +86,6 @@ describe('/memories', () => { }); describe('PUT /memories/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}/assets`) - .send({ ids: [userAsset1.id] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .put(`/memories/${uuidDto.invalid}/assets`) - .send({ ids: [userAsset1.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .put(`/memories/${userMemory.id}/assets`) @@ -244,15 +95,6 @@ describe('/memories', () => { expect(body).toEqual(errorDto.noPermission); }); - it('should require a valid asset id', async () => { - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}/assets`) - .send({ ids: [uuidDto.invalid] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); - }); - it('should require asset access', async () => { const { status, body } = await request(app) .put(`/memories/${userMemory.id}/assets`) @@ -279,23 +121,6 @@ describe('/memories', () => { }); describe('DELETE /memories/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .delete(`/memories/${userMemory.id}/assets`) - .send({ ids: [userAsset1.id] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .delete(`/memories/${uuidDto.invalid}/assets`) - .send({ ids: [userAsset1.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .delete(`/memories/${userMemory.id}/assets`) @@ -305,15 +130,6 @@ describe('/memories', () => { expect(body).toEqual(errorDto.noPermission); }); - it('should require a valid asset id', async () => { - const { status, body } = await request(app) - .delete(`/memories/${userMemory.id}/assets`) - .send({ ids: [uuidDto.invalid] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); - }); - it('should only remove assets in the memory', async () => { const { status, body } = await request(app) .delete(`/memories/${userMemory.id}/assets`) @@ -340,21 +156,6 @@ describe('/memories', () => { }); describe('DELETE /memories/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .delete(`/memories/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .delete(`/memories/${userMemory.id}`) diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts new file mode 100644 index 0000000000..ac96e54a5b --- /dev/null +++ b/server/src/controllers/memory.controller.spec.ts @@ -0,0 +1,132 @@ +import { MemoryController } from 'src/controllers/memory.controller'; +import { MemoryService } from 'src/services/memory.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(MemoryController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(MemoryService); + + beforeAll(async () => { + ctx = await controllerSetup(MemoryController, [{ provide: MemoryService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /memories', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/memories'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /memories', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/memories'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should validate data when type is on this day', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/memories') + .send({ + type: 'on_this_day', + data: {}, + memoryAt: new Date(2021).toISOString(), + }); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), + ); + }); + }); + + describe('GET /memories/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/memories/statistics'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /memories/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/memories/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + }); + + describe('PUT /memories/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + }); + + describe('DELETE /memories/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /memories/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/memories/${factory.uuid()}/assets`) + .send({ ids: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + }); + + describe('DELETE /memories/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete(`/memories/${factory.uuid()}/assets`) + .send({ ids: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + }); +}); diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index ff7249d930..53ab970d47 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -1,7 +1,8 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { DB } from 'src/db'; -import { AssetFileType } from 'src/enum'; +import { AssetFileType, MemoryType } from 'src/enum'; +import { AccessRepository } from 'src/repositories/access.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -11,6 +12,7 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos import { UserRepository } from 'src/repositories/user.repository'; import { MemoryService } from 'src/services/memory.service'; import { newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; @@ -19,6 +21,7 @@ const setup = (db?: Kysely) => { return newMediumService(MemoryService, { database: db || defaultDatabase, real: [ + AccessRepository, AssetRepository, DatabaseRepository, MemoryRepository, @@ -36,6 +39,74 @@ describe(MemoryService.name, () => { defaultDatabase = await getKyselyDB(); }); + describe('create', () => { + it('should create a new memory', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const dto = { + type: MemoryType.ON_THIS_DAY, + data: { year: 2021 }, + memoryAt: new Date(2021), + }; + + await expect(sut.create(auth, dto)).resolves.toEqual({ + id: expect.any(String), + type: dto.type, + data: dto.data, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + isSaved: false, + memoryAt: dto.memoryAt, + ownerId: user.id, + assets: [], + }); + }); + + it('should create a new memory (with assets)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); + const auth = factory.auth({ user }); + const dto = { + type: MemoryType.ON_THIS_DAY, + data: { year: 2021 }, + memoryAt: new Date(2021), + assetIds: [asset1.id, asset2.id], + }; + + await expect(sut.create(auth, dto)).resolves.toEqual( + expect.objectContaining({ + id: expect.any(String), + assets: [expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })], + }), + ); + }); + + it('should create a new memory and ignore assets the user does not have access to', async () => { + const { sut, ctx } = setup(); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user1.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id }); + const auth = factory.auth({ user: user1 }); + const dto = { + type: MemoryType.ON_THIS_DAY, + data: { year: 2021 }, + memoryAt: new Date(2021), + assetIds: [asset1.id, asset2.id], + }; + + await expect(sut.create(auth, dto)).resolves.toEqual( + expect.objectContaining({ + id: expect.any(String), + assets: [expect.objectContaining({ id: asset1.id })], + }), + ); + }); + }); + describe('onMemoryCreate', () => { it('should work on an empty database', async () => { const { sut } = setup();