From 2f48a8aab48e8dd805a0eb7c26b622b2661a33f9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 21 Aug 2026 12:15:17 -0400 Subject: [PATCH] refactor: memory e2e (#30906) --- e2e/src/specs/server/api/memory.e2e-spec.ts | 131 ------------------ .../specs/services/memory.service.spec.ts | 99 +++++++++++-- 2 files changed, 90 insertions(+), 140 deletions(-) delete mode 100644 e2e/src/specs/server/api/memory.e2e-spec.ts diff --git a/e2e/src/specs/server/api/memory.e2e-spec.ts b/e2e/src/specs/server/api/memory.e2e-spec.ts deleted file mode 100644 index 38c6ba875..000000000 --- a/e2e/src/specs/server/api/memory.e2e-spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - AssetMediaResponseDto, - LoginResponseDto, - MemoryResponseDto, - MemoryType, - createMemory, - getMemory, -} from '@immich/sdk'; -import { createUserDto } from 'src/fixtures'; -import { app, asBearerAuth, utils } from 'src/utils'; -import request from 'supertest'; -import { beforeAll, describe, expect, it } from 'vitest'; - -describe('/memories', () => { - let admin: LoginResponseDto; - let user: LoginResponseDto; - let adminAsset: AssetMediaResponseDto; - let userAsset1: AssetMediaResponseDto; - let userMemory: MemoryResponseDto; - - beforeAll(async () => { - await utils.resetDatabase(); - - admin = await utils.adminSetup(); - user = await utils.userSetup(admin.accessToken, createUserDto.user1); - [adminAsset, userAsset1] = await Promise.all([ - utils.createAsset(admin.accessToken), - utils.createAsset(user.accessToken), - ]); - userMemory = await createMemory( - { - memoryCreateDto: { - type: MemoryType.OnThisDay, - memoryAt: new Date(2021).toISOString(), - data: { year: 2021 }, - assetIds: [], - }, - }, - { headers: asBearerAuth(user.accessToken) }, - ); - }); - - describe('GET /memories/:id', () => { - it('should get the memory', async () => { - const { status, body } = await request(app) - .get(`/memories/${userMemory.id}`) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ id: userMemory.id }); - }); - }); - - describe('PUT /memories/:id', () => { - it('should update the memory', async () => { - const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) }); - expect(before.isSaved).toBe(false); - - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}`) - .send({ isSaved: true }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: userMemory.id, - isSaved: true, - }); - }); - }); - - describe('PUT /memories/:id/assets', () => { - it('should require asset access', async () => { - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}/assets`) - .send({ ids: [adminAsset.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body[0]).toEqual({ - id: adminAsset.id, - success: false, - error: 'no_permission', - }); - }); - - it('should add assets to the memory', async () => { - const { status, body } = await request(app) - .put(`/memories/${userMemory.id}/assets`) - .send({ ids: [userAsset1.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body[0]).toEqual({ id: userAsset1.id, success: true }); - }); - }); - - describe('DELETE /memories/:id/assets', () => { - it('should only remove assets in the memory', async () => { - const { status, body } = await request(app) - .delete(`/memories/${userMemory.id}/assets`) - .send({ ids: [adminAsset.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body[0]).toEqual({ - id: adminAsset.id, - success: false, - error: 'not_found', - }); - }); - - it('should remove assets from the memory', async () => { - const { status, body } = await request(app) - .delete(`/memories/${userMemory.id}/assets`) - .send({ ids: [userAsset1.id] }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body[0]).toEqual({ id: userAsset1.id, success: true }); - }); - }); - - describe('DELETE /memories/:id', () => { - it('should delete the memory', async () => { - const { status } = await request(app) - .delete(`/memories/${userMemory.id}`) - .send({ isSaved: true }) - .set('Authorization', `Bearer ${user.accessToken}`); - expect(status).toBe(204); - }); - }); -}); diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index 5a9217ea6..f4a6e341b 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AssetFileType, MemoryType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; @@ -34,30 +35,51 @@ const setup = (db?: Kysely) => { }); }; -/** A memory owned by one user, plus another user's auth to attempt access with */ -const newMemoryOfAnotherUser = async (ctx: ReturnType['ctx']) => { +const create = async (ctx: ReturnType['ctx']) => { const { user } = await ctx.newUser(); - const { user: otherUser } = await ctx.newUser(); const { memory } = await ctx.newMemory({ ownerId: user.id }); const { asset } = await ctx.newAsset({ ownerId: user.id }); - return { memory, asset, auth: factory.auth({ user }), otherAuth: factory.auth({ user: otherUser }) }; + return { memory, asset, user }; }; describe(MemoryService.name, () => { describe('get', () => { + it('should return the memory', async () => { + const { sut, ctx } = setup(); + const { memory, user } = await create(ctx); + const auth = factory.auth({ user }); + + await expect(sut.get(auth, memory.id)).resolves.toEqual(expect.objectContaining({ id: memory.id })); + }); + it('should not return a memory of another user', async () => { const { sut, ctx } = setup(); - const { memory, otherAuth } = await newMemoryOfAnotherUser(ctx); + const { memory } = await create(ctx); + const { user: otherUser } = await ctx.newUser(); + const otherAuth = factory.auth({ user: otherUser }); await expect(sut.get(otherAuth, memory.id)).rejects.toThrow('Not found or no memory.read access'); }); }); describe('update', () => { + it('should update the memory', async () => { + const { sut, ctx } = setup(); + const { memory, user } = await create(ctx); + const auth = factory.auth({ user }); + + await expect(sut.get(auth, memory.id)).resolves.toEqual(expect.objectContaining({ isSaved: false })); + await expect(sut.update(auth, memory.id, { isSaved: true })).resolves.toEqual( + expect.objectContaining({ id: memory.id, isSaved: true }), + ); + }); + it('should not update a memory of another user', async () => { const { sut, ctx } = setup(); - const { memory, otherAuth } = await newMemoryOfAnotherUser(ctx); + const { memory } = await create(ctx); + const { user: otherUser } = await ctx.newUser(); + const otherAuth = factory.auth({ user: otherUser }); await expect(sut.update(otherAuth, memory.id, { isSaved: true })).rejects.toThrow( 'Not found or no memory.update access', @@ -66,9 +88,21 @@ describe(MemoryService.name, () => { }); describe('remove', () => { + it('should remove the memory', async () => { + const { sut, ctx } = setup(); + const { memory, user } = await create(ctx); + const auth = factory.auth({ user }); + + await expect(sut.remove(auth, memory.id)).resolves.toBeUndefined(); + await expect(sut.get(auth, memory.id)).rejects.toThrow('Not found or no memory.read access'); + }); + it('should not remove a memory of another user', async () => { const { sut, ctx } = setup(); - const { memory, auth, otherAuth } = await newMemoryOfAnotherUser(ctx); + const { memory, user } = await create(ctx); + const auth = factory.auth({ user }); + const { user: otherUser } = await ctx.newUser(); + const otherAuth = factory.auth({ user: otherUser }); await expect(sut.remove(otherAuth, memory.id)).rejects.toThrow('Not found or no memory.delete access'); await expect(sut.get(auth, memory.id)).resolves.toEqual(expect.objectContaining({ id: memory.id })); @@ -76,9 +110,33 @@ describe(MemoryService.name, () => { }); describe('addAssets', () => { + it('should add assets to the memory', async () => { + const { sut, ctx } = setup(); + const { memory, asset, user } = await create(ctx); + const auth = factory.auth({ user }); + + await expect(sut.addAssets(auth, memory.id, { ids: [asset.id] })).resolves.toEqual([ + { id: asset.id, success: true }, + ]); + }); + + it('should require access to the asset', async () => { + const { sut, ctx } = setup(); + const { memory, user } = await create(ctx); + const auth = factory.auth({ user }); + const { user: other } = await ctx.newUser(); + const { asset: otherAsset } = await ctx.newAsset({ ownerId: other.id }); + + await expect(sut.addAssets(auth, memory.id, { ids: [otherAsset.id] })).resolves.toEqual([ + { id: otherAsset.id, success: false, error: BulkIdErrorReason.NO_PERMISSION }, + ]); + }); + it('should not add assets to a memory of another user', async () => { const { sut, ctx } = setup(); - const { memory, asset, otherAuth } = await newMemoryOfAnotherUser(ctx); + const { memory, asset } = await create(ctx); + const { user: otherUser } = await ctx.newUser(); + const otherAuth = factory.auth({ user: otherUser }); await expect(sut.addAssets(otherAuth, memory.id, { ids: [asset.id] })).rejects.toThrow( 'Not found or no memory.read access', @@ -87,9 +145,32 @@ describe(MemoryService.name, () => { }); describe('removeAssets', () => { + it('should remove assets from the memory', async () => { + const { sut, ctx } = setup(); + const { memory, asset, user } = await create(ctx); + const auth = factory.auth({ user }); + await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); + + await expect(sut.removeAssets(auth, memory.id, { ids: [asset.id] })).resolves.toEqual([ + { id: asset.id, success: true }, + ]); + }); + + it('should only remove assets that are in the memory', async () => { + const { sut, ctx } = setup(); + const { memory, asset, user } = await create(ctx); + const auth = factory.auth({ user }); + + await expect(sut.removeAssets(auth, memory.id, { ids: [asset.id] })).resolves.toEqual([ + { id: asset.id, success: false, error: BulkIdErrorReason.NOT_FOUND }, + ]); + }); + it('should not remove assets from a memory of another user', async () => { const { sut, ctx } = setup(); - const { memory, asset, otherAuth } = await newMemoryOfAnotherUser(ctx); + const { memory, asset } = await create(ctx); + const { user: otherUser } = await ctx.newUser(); + const otherAuth = factory.auth({ user: otherUser }); await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); await expect(sut.removeAssets(otherAuth, memory.id, { ids: [asset.id] })).rejects.toThrow(