Skip to content

Commit 9d0f038

Browse files
chore: finishing unit tests for a couple of services (immich-app#13292)
1 parent f5e0cde commit 9d0f038

17 files changed

+386
-8
lines changed

server/src/services/api-key.service.spec.ts

+9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ describe(APIKeyService.name, () => {
4646
expect(cryptoMock.newPassword).toHaveBeenCalled();
4747
expect(cryptoMock.hashSha256).toHaveBeenCalled();
4848
});
49+
50+
it('should throw an error if the api key does not have sufficient permissions', async () => {
51+
await expect(
52+
sut.create(
53+
{ ...authStub.admin, apiKey: { ...keyStub.admin, permissions: [] } },
54+
{ permissions: [Permission.ASSET_READ] },
55+
),
56+
).rejects.toBeInstanceOf(BadRequestException);
57+
});
4958
});
5059

5160
describe('update', () => {

server/src/services/asset.service.spec.ts

+6
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,12 @@ describe(AssetService.name, () => {
583583
});
584584

585585
describe('run', () => {
586+
it('should run the refresh faces job', async () => {
587+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
588+
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES });
589+
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]);
590+
});
591+
586592
it('should run the refresh metadata job', async () => {
587593
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
588594
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA });

server/src/services/auth.service.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ describe('AuthService', () => {
7272
expect(sut).toBeDefined();
7373
});
7474

75+
describe('onBootstrap', () => {
76+
it('should init the repo', () => {
77+
sut.onBootstrap();
78+
expect(oauthMock.init).toHaveBeenCalled();
79+
});
80+
});
81+
7582
describe('login', () => {
7683
it('should throw an error if password login is disabled', async () => {
7784
systemMock.get.mockResolvedValue(systemConfigStub.disabled);

server/src/services/download.service.spec.ts

+47-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
22
import { DownloadResponseDto } from 'src/dtos/download.dto';
33
import { AssetEntity } from 'src/entities/asset.entity';
44
import { IAssetRepository } from 'src/interfaces/asset.interface';
5+
import { ILoggerRepository } from 'src/interfaces/logger.interface';
56
import { IStorageRepository } from 'src/interfaces/storage.interface';
67
import { DownloadService } from 'src/services/download.service';
78
import { assetStub } from 'test/fixtures/asset.stub';
@@ -25,17 +26,62 @@ describe(DownloadService.name, () => {
2526
let sut: DownloadService;
2627
let accessMock: IAccessRepositoryMock;
2728
let assetMock: Mocked<IAssetRepository>;
29+
let loggerMock: Mocked<ILoggerRepository>;
2830
let storageMock: Mocked<IStorageRepository>;
2931

3032
it('should work', () => {
3133
expect(sut).toBeDefined();
3234
});
3335

3436
beforeEach(() => {
35-
({ sut, accessMock, assetMock, storageMock } = newTestService(DownloadService));
37+
({ sut, accessMock, assetMock, loggerMock, storageMock } = newTestService(DownloadService));
3638
});
3739

3840
describe('downloadArchive', () => {
41+
it('should skip asset ids that could not be found', async () => {
42+
const archiveMock = {
43+
addFile: vitest.fn(),
44+
finalize: vitest.fn(),
45+
stream: new Readable(),
46+
};
47+
48+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
49+
assetMock.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]);
50+
storageMock.createZipStream.mockReturnValue(archiveMock);
51+
52+
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
53+
stream: archiveMock.stream,
54+
});
55+
56+
expect(archiveMock.addFile).toHaveBeenCalledTimes(1);
57+
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
58+
});
59+
60+
it('should log a warning if the original path could not be resolved', async () => {
61+
const archiveMock = {
62+
addFile: vitest.fn(),
63+
finalize: vitest.fn(),
64+
stream: new Readable(),
65+
};
66+
67+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
68+
storageMock.realpath.mockRejectedValue(new Error('Could not read file'));
69+
assetMock.getByIds.mockResolvedValue([
70+
{ ...assetStub.noResizePath, id: 'asset-1' },
71+
{ ...assetStub.noWebpPath, id: 'asset-2' },
72+
]);
73+
storageMock.createZipStream.mockReturnValue(archiveMock);
74+
75+
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
76+
stream: archiveMock.stream,
77+
});
78+
79+
expect(loggerMock.warn).toHaveBeenCalledTimes(2);
80+
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
81+
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
82+
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg');
83+
});
84+
3985
it('should download an archive', async () => {
4086
const archiveMock = {
4187
addFile: vitest.fn(),

server/src/services/duplicate.service.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
66
import { DuplicateService } from 'src/services/duplicate.service';
77
import { SearchService } from 'src/services/search.service';
88
import { assetStub } from 'test/fixtures/asset.stub';
9+
import { authStub } from 'test/fixtures/auth.stub';
910
import { newTestService } from 'test/utils';
1011
import { Mocked, beforeEach, vitest } from 'vitest';
1112

@@ -28,6 +29,15 @@ describe(SearchService.name, () => {
2829
expect(sut).toBeDefined();
2930
});
3031

32+
describe('getDuplicates', () => {
33+
it('should get duplicates', async () => {
34+
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
35+
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
36+
{ duplicateId: assetStub.hasDupe.duplicateId, assets: [expect.objectContaining({ id: assetStub.hasDupe.id })] },
37+
]);
38+
});
39+
});
40+
3141
describe('handleQueueSearchDuplicates', () => {
3242
beforeEach(() => {
3343
systemMock.get.mockResolvedValue({

server/src/services/map.service.spec.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
import { IAlbumRepository } from 'src/interfaces/album.interface';
12
import { IMapRepository } from 'src/interfaces/map.interface';
23
import { IPartnerRepository } from 'src/interfaces/partner.interface';
34
import { MapService } from 'src/services/map.service';
5+
import { albumStub } from 'test/fixtures/album.stub';
46
import { assetStub } from 'test/fixtures/asset.stub';
57
import { authStub } from 'test/fixtures/auth.stub';
8+
import { partnerStub } from 'test/fixtures/partner.stub';
69
import { newTestService } from 'test/utils';
710
import { Mocked } from 'vitest';
811

912
describe(MapService.name, () => {
1013
let sut: MapService;
1114

15+
let albumMock: Mocked<IAlbumRepository>;
1216
let mapMock: Mocked<IMapRepository>;
1317
let partnerMock: Mocked<IPartnerRepository>;
1418

1519
beforeEach(() => {
16-
({ sut, mapMock, partnerMock } = newTestService(MapService));
20+
({ sut, albumMock, mapMock, partnerMock } = newTestService(MapService));
1721
});
1822

1923
describe('getMapMarkers', () => {
@@ -35,5 +39,62 @@ describe(MapService.name, () => {
3539
expect(markers).toHaveLength(1);
3640
expect(markers[0]).toEqual(marker);
3741
});
42+
43+
it('should include partner assets', async () => {
44+
const asset = assetStub.withLocation;
45+
const marker = {
46+
id: asset.id,
47+
lat: asset.exifInfo!.latitude!,
48+
lon: asset.exifInfo!.longitude!,
49+
city: asset.exifInfo!.city,
50+
state: asset.exifInfo!.state,
51+
country: asset.exifInfo!.country,
52+
};
53+
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
54+
mapMock.getMapMarkers.mockResolvedValue([marker]);
55+
56+
const markers = await sut.getMapMarkers(authStub.user1, { withPartners: true });
57+
58+
expect(mapMock.getMapMarkers).toHaveBeenCalledWith(
59+
[authStub.user1.user.id, partnerStub.adminToUser1.sharedById],
60+
expect.arrayContaining([]),
61+
{ withPartners: true },
62+
);
63+
expect(markers).toHaveLength(1);
64+
expect(markers[0]).toEqual(marker);
65+
});
66+
67+
it('should include assets from shared albums', async () => {
68+
const asset = assetStub.withLocation;
69+
const marker = {
70+
id: asset.id,
71+
lat: asset.exifInfo!.latitude!,
72+
lon: asset.exifInfo!.longitude!,
73+
city: asset.exifInfo!.city,
74+
state: asset.exifInfo!.state,
75+
country: asset.exifInfo!.country,
76+
};
77+
partnerMock.getAll.mockResolvedValue([]);
78+
mapMock.getMapMarkers.mockResolvedValue([marker]);
79+
albumMock.getOwned.mockResolvedValue([albumStub.empty]);
80+
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
81+
82+
const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true });
83+
84+
expect(markers).toHaveLength(1);
85+
expect(markers[0]).toEqual(marker);
86+
});
87+
});
88+
89+
describe('reverseGeocode', () => {
90+
it('should reverse geocode a location', async () => {
91+
mapMock.reverseGeocode.mockResolvedValue({ city: 'foo', state: 'bar', country: 'baz' });
92+
93+
await expect(sut.reverseGeocode({ lat: 42, lon: 69 })).resolves.toEqual([
94+
{ city: 'foo', state: 'bar', country: 'baz' },
95+
]);
96+
97+
expect(mapMock.reverseGeocode).toHaveBeenCalledWith({ latitude: 42, longitude: 69 });
98+
});
3899
});
39100
});

server/src/services/notification.service.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ describe(NotificationService.name, () => {
126126
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
127127
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
128128
});
129+
130+
it('should fail if smtp configuration is invalid', async () => {
131+
const oldConfig = configs.smtpDisabled;
132+
const newConfig = configs.smtpEnabled;
133+
134+
notificationMock.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
135+
await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error);
136+
});
129137
});
130138

131139
describe('onAssetHide', () => {
@@ -180,6 +188,18 @@ describe(NotificationService.name, () => {
180188
});
181189
});
182190

191+
describe('onSessionDeleteEvent', () => {
192+
it('should send a on_session_delete client event', () => {
193+
vi.useFakeTimers();
194+
sut.onSessionDelete({ sessionId: 'id' });
195+
expect(eventMock.clientSend).not.toHaveBeenCalled();
196+
197+
vi.advanceTimersByTime(500);
198+
199+
expect(eventMock.clientSend).toHaveBeenCalledWith('on_session_delete', 'id', 'id');
200+
});
201+
});
202+
183203
describe('onAssetTrash', () => {
184204
it('should send connected clients an event', () => {
185205
sut.onAssetTrash({ assetId: 'asset-id', userId: 'user-id' });

server/src/services/partner.service.spec.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.int
33
import { PartnerService } from 'src/services/partner.service';
44
import { authStub } from 'test/fixtures/auth.stub';
55
import { partnerStub } from 'test/fixtures/partner.stub';
6+
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
67
import { newTestService } from 'test/utils';
78
import { Mocked } from 'vitest';
89

910
describe(PartnerService.name, () => {
1011
let sut: PartnerService;
12+
13+
let accessMock: IAccessRepositoryMock;
1114
let partnerMock: Mocked<IPartnerRepository>;
1215

1316
beforeEach(() => {
14-
({ sut, partnerMock } = newTestService(PartnerService));
17+
({ sut, accessMock, partnerMock } = newTestService(PartnerService));
1518
});
1619

1720
it('should work', () => {
@@ -71,4 +74,24 @@ describe(PartnerService.name, () => {
7174
expect(partnerMock.remove).not.toHaveBeenCalled();
7275
});
7376
});
77+
78+
describe('update', () => {
79+
it('should require access', async () => {
80+
await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: false })).rejects.toBeInstanceOf(
81+
BadRequestException,
82+
);
83+
});
84+
85+
it('should update partner', async () => {
86+
accessMock.partner.checkUpdateAccess.mockResolvedValue(new Set(['shared-by-id']));
87+
partnerMock.update.mockResolvedValue(partnerStub.adminToUser1);
88+
89+
await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: true })).resolves.toBeDefined();
90+
expect(partnerMock.update).toHaveBeenCalledWith({
91+
sharedById: 'shared-by-id',
92+
sharedWithId: authStub.admin.user.id,
93+
inTimeline: true,
94+
});
95+
});
96+
});
7497
});

server/src/services/shared-link.service.spec.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,21 @@ describe(SharedLinkService.name, () => {
5858
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
5959
});
6060

61-
it('should throw an error for an password protected shared link', async () => {
61+
it('should throw an error for an invalid password protected shared link', async () => {
6262
const authDto = authStub.adminSharedLink;
6363
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
6464
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException);
6565
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
6666
});
67+
68+
it('should allow a correct password on a password protected shared link', async () => {
69+
sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
70+
await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined();
71+
expect(sharedLinkMock.get).toHaveBeenCalledWith(
72+
authStub.adminSharedLink.user.id,
73+
authStub.adminSharedLink.sharedLink?.id,
74+
);
75+
});
6776
});
6877

6978
describe('get', () => {
@@ -300,5 +309,15 @@ describe(SharedLinkService.name, () => {
300309
});
301310
expect(sharedLinkMock.get).toHaveBeenCalled();
302311
});
312+
313+
it('should return metadata tags with a default image path if the asset id is not set', async () => {
314+
sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
315+
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
316+
description: '0 shared photos & videos',
317+
imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/feature-panel.png`,
318+
title: 'Public Share',
319+
});
320+
expect(sharedLinkMock.get).toHaveBeenCalled();
321+
});
303322
});
304323
});

server/src/services/shared-link.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { OpenGraphTags } from 'src/utils/misc';
2020

2121
@Injectable()
2222
export class SharedLinkService extends BaseService {
23-
getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
23+
async getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
2424
return this.sharedLinkRepository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link)));
2525
}
2626

0 commit comments

Comments
 (0)