diff --git a/server/src/database.ts b/server/src/database.ts index 46d33d916d..92f6c5f702 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -15,6 +15,14 @@ export type AuthApiKey = { permissions: Permission[]; }; +export type User = { + id: string; + name: string; + email: string; + profileImagePath: string; + profileChangedAt: Date; +}; + export type AuthSharedLink = { id: string; expiresAt: Date | null; diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index bb63c7bf7b..368c8f2575 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -1,8 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { ReactionType } from 'src/dtos/activity.dto'; import { ActivityService } from 'src/services/activity.service'; -import { activityStub } from 'test/fixtures/activity.stub'; -import { authStub } from 'test/fixtures/auth.stub'; +import { factory, newUuid, newUuids } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(ActivityService.name, () => { @@ -19,137 +18,118 @@ describe(ActivityService.name, () => { describe('getAll', () => { it('should get all', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); + const [albumId, assetId, userId] = newUuids(); + + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.activity.search.mockResolvedValue([]); - await expect(sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id' })).resolves.toEqual([]); + await expect(sut.getAll(factory.auth({ id: userId }), { assetId, albumId })).resolves.toEqual([]); - expect(mocks.activity.search).toHaveBeenCalledWith({ - assetId: 'asset-id', - albumId: 'album-id', - isLiked: undefined, - }); + expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: undefined }); }); it('should filter by type=like', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); + const [albumId, assetId, userId] = newUuids(); + + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.activity.search.mockResolvedValue([]); await expect( - sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id', type: ReactionType.LIKE }), + sut.getAll(factory.auth({ id: userId }), { assetId, albumId, type: ReactionType.LIKE }), ).resolves.toEqual([]); - expect(mocks.activity.search).toHaveBeenCalledWith({ - assetId: 'asset-id', - albumId: 'album-id', - isLiked: true, - }); + expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: true }); }); it('should filter by type=comment', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); + const [albumId, assetId] = newUuids(); + + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.activity.search.mockResolvedValue([]); - await expect( - sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id', type: ReactionType.COMMENT }), - ).resolves.toEqual([]); + await expect(sut.getAll(factory.auth(), { assetId, albumId, type: ReactionType.COMMENT })).resolves.toEqual([]); - expect(mocks.activity.search).toHaveBeenCalledWith({ - assetId: 'asset-id', - albumId: 'album-id', - isLiked: false, - }); + expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: false }); }); }); describe('getStatistics', () => { it('should get the comment count', async () => { + const [albumId, assetId] = newUuids(); + mocks.activity.getStatistics.mockResolvedValue(1); - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([activityStub.oneComment.albumId])); - await expect( - sut.getStatistics(authStub.admin, { - assetId: 'asset-id', - albumId: activityStub.oneComment.albumId, - }), - ).resolves.toEqual({ comments: 1 }); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); + + await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1 }); }); }); describe('addComment', () => { it('should require access to the album', async () => { + const [albumId, assetId] = newUuids(); + await expect( - sut.create(authStub.admin, { - albumId: 'album-id', - assetId: 'asset-id', - type: ReactionType.COMMENT, - comment: 'comment', - }), + sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should create a comment', async () => { - mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id'])); - mocks.activity.create.mockResolvedValue(activityStub.oneComment); + const [albumId, assetId, userId] = newUuids(); + const activity = factory.activity({ albumId, assetId, userId }); - await sut.create(authStub.admin, { - albumId: 'album-id', - assetId: 'asset-id', + mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId])); + mocks.activity.create.mockResolvedValue(activity); + + await sut.create(factory.auth({ id: userId }), { + albumId, + assetId, type: ReactionType.COMMENT, comment: 'comment', }); expect(mocks.activity.create).toHaveBeenCalledWith({ - userId: 'admin_id', - albumId: 'album-id', - assetId: 'asset-id', + userId: activity.userId, + albumId: activity.albumId, + assetId: activity.assetId, comment: 'comment', isLiked: false, }); }); it('should fail because activity is disabled for the album', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); - mocks.activity.create.mockResolvedValue(activityStub.oneComment); + const [albumId, assetId] = newUuids(); + const activity = factory.activity({ albumId, assetId }); + + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); + mocks.activity.create.mockResolvedValue(activity); await expect( - sut.create(authStub.admin, { - albumId: 'album-id', - assetId: 'asset-id', - type: ReactionType.COMMENT, - comment: 'comment', - }), + sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should create a like', async () => { - mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id'])); - mocks.activity.create.mockResolvedValue(activityStub.liked); + const [albumId, assetId, userId] = newUuids(); + const activity = factory.activity({ userId, albumId, assetId, isLiked: true }); + + mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId])); + mocks.activity.create.mockResolvedValue(activity); mocks.activity.search.mockResolvedValue([]); - await sut.create(authStub.admin, { - albumId: 'album-id', - assetId: 'asset-id', - type: ReactionType.LIKE, - }); + await sut.create(factory.auth({ id: userId }), { albumId, assetId, type: ReactionType.LIKE }); - expect(mocks.activity.create).toHaveBeenCalledWith({ - userId: 'admin_id', - albumId: 'album-id', - assetId: 'asset-id', - isLiked: true, - }); + expect(mocks.activity.create).toHaveBeenCalledWith({ userId: activity.userId, albumId, assetId, isLiked: true }); }); it('should skip if like exists', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); - mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id'])); - mocks.activity.search.mockResolvedValue([activityStub.liked]); + const [albumId, assetId] = newUuids(); + const activity = factory.activity({ albumId, assetId, isLiked: true }); - await sut.create(authStub.admin, { - albumId: 'album-id', - assetId: 'asset-id', - type: ReactionType.LIKE, - }); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); + mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId])); + mocks.activity.search.mockResolvedValue([activity]); + + await sut.create(factory.auth(), { albumId, assetId, type: ReactionType.LIKE }); expect(mocks.activity.create).not.toHaveBeenCalled(); }); @@ -157,20 +137,29 @@ describe(ActivityService.name, () => { describe('delete', () => { it('should require access', async () => { - await expect(sut.delete(authStub.admin, activityStub.oneComment.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.delete(factory.auth(), newUuid())).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.activity.delete).not.toHaveBeenCalled(); }); it('should let the activity owner delete a comment', async () => { - mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set(['activity-id'])); - await sut.delete(authStub.admin, 'activity-id'); - expect(mocks.activity.delete).toHaveBeenCalledWith('activity-id'); + const activity = factory.activity(); + + mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set([activity.id])); + + await sut.delete(factory.auth(), activity.id); + + expect(mocks.activity.delete).toHaveBeenCalledWith(activity.id); }); it('should let the album owner delete a comment', async () => { - mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set(['activity-id'])); - await sut.delete(authStub.admin, 'activity-id'); - expect(mocks.activity.delete).toHaveBeenCalledWith('activity-id'); + const activity = factory.activity(); + + mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set([activity.id])); + + await sut.delete(factory.auth(), activity.id); + + expect(mocks.activity.delete).toHaveBeenCalledWith(activity.id); }); }); }); diff --git a/server/test/factory.ts b/server/test/factory.ts index 8811b08628..9cb2c818ab 100644 --- a/server/test/factory.ts +++ b/server/test/factory.ts @@ -1,5 +1,5 @@ import { Insertable, Kysely } from 'kysely'; -import { randomBytes, randomUUID } from 'node:crypto'; +import { randomBytes } from 'node:crypto'; import { Writable } from 'node:stream'; import { Assets, DB, Partners, Sessions, Users } from 'src/db'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -36,6 +36,7 @@ import { VersionHistoryRepository } from 'src/repositories/version-history.repos import { ViewRepository } from 'src/repositories/view-repository'; import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; +import { newUuid } from 'test/small.factory'; class CustomWritable extends Writable { private data = ''; @@ -59,8 +60,6 @@ type User = Partial>; type Session = Omit, 'token'> & { token?: string }; type Partner = Insertable; -export const newUuid = () => randomUUID() as string; - export class TestFactory { private assets: Asset[] = []; private sessions: Session[] = []; diff --git a/server/test/fixtures/activity.stub.ts b/server/test/fixtures/activity.stub.ts deleted file mode 100644 index a81fd51ca8..0000000000 --- a/server/test/fixtures/activity.stub.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ActivityItem } from 'src/types'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; - -export const activityStub = { - oneComment: Object.freeze({ - id: 'activity-1', - comment: 'comment', - isLiked: false, - userId: 'admin_id', - user: { - id: 'admin_id', - name: 'admin', - email: 'admin@test.com', - profileImagePath: '', - profileChangedAt: new Date('2021-01-01'), - }, - assetId: assetStub.image.id, - albumId: albumStub.oneAsset.id, - createdAt: new Date(), - updatedAt: new Date(), - updateId: 'uuid-v7', - }), - liked: Object.freeze({ - id: 'activity-2', - comment: null, - isLiked: true, - userId: 'admin_id', - user: { - id: 'admin_id', - name: 'admin', - email: 'admin@test.com', - profileImagePath: '', - profileChangedAt: new Date('2021-01-01'), - }, - assetId: assetStub.image.id, - albumId: albumStub.oneAsset.id, - createdAt: new Date(), - updatedAt: new Date(), - updateId: 'uuid-v7', - }), -}; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts new file mode 100644 index 0000000000..5901caa88f --- /dev/null +++ b/server/test/small.factory.ts @@ -0,0 +1,54 @@ +import { randomUUID } from 'node:crypto'; +import { AuthUser, User } from 'src/database'; +import { ActivityItem } from 'src/types'; + +export const newUuid = () => randomUUID() as string; +export const newUuids = () => + Array.from({ length: 100 }) + .fill(0) + .map(() => newUuid()); +export const newDate = () => new Date(); +export const newUpdateId = () => 'uuid-v7'; + +const authUser = (authUser: Partial) => ({ + id: newUuid(), + isAdmin: false, + name: 'Test User', + email: 'test@immich.cloud', + quotaUsageInBytes: 0, + quotaSizeInBytes: null, + ...authUser, +}); + +const user = (user: Partial) => ({ + id: newUuid(), + name: 'Test User', + email: 'test@immich.cloud', + profileImagePath: '', + profileChangedAt: newDate(), + ...user, +}); + +export const factory = { + auth: (user: Partial = {}) => ({ + user: authUser(user), + }), + authUser, + user, + activity: (activity: Partial = {}) => { + const userId = activity.userId || newUuid(); + return { + id: newUuid(), + comment: null, + isLiked: false, + userId, + user: user({ id: userId }), + assetId: newUuid(), + albumId: newUuid(), + createdAt: newDate(), + updatedAt: newDate(), + updateId: newUpdateId(), + ...activity, + }; + }, +};