Skip to content

Commit

Permalink
chore: finishing unit tests for a couple of services (immich-app#13292)
Browse files Browse the repository at this point in the history
  • Loading branch information
danieldietzler authored Oct 8, 2024
1 parent f5e0cde commit 9d0f038
Show file tree
Hide file tree
Showing 17 changed files with 386 additions and 8 deletions.
9 changes: 9 additions & 0 deletions server/src/services/api-key.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ describe(APIKeyService.name, () => {
expect(cryptoMock.newPassword).toHaveBeenCalled();
expect(cryptoMock.hashSha256).toHaveBeenCalled();
});

it('should throw an error if the api key does not have sufficient permissions', async () => {
await expect(
sut.create(
{ ...authStub.admin, apiKey: { ...keyStub.admin, permissions: [] } },
{ permissions: [Permission.ASSET_READ] },
),
).rejects.toBeInstanceOf(BadRequestException);
});
});

describe('update', () => {
Expand Down
6 changes: 6 additions & 0 deletions server/src/services/asset.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,12 @@ describe(AssetService.name, () => {
});

describe('run', () => {
it('should run the refresh faces job', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES });
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]);
});

it('should run the refresh metadata job', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA });
Expand Down
7 changes: 7 additions & 0 deletions server/src/services/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ describe('AuthService', () => {
expect(sut).toBeDefined();
});

describe('onBootstrap', () => {
it('should init the repo', () => {
sut.onBootstrap();
expect(oauthMock.init).toHaveBeenCalled();
});
});

describe('login', () => {
it('should throw an error if password login is disabled', async () => {
systemMock.get.mockResolvedValue(systemConfigStub.disabled);
Expand Down
48 changes: 47 additions & 1 deletion server/src/services/download.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
import { DownloadResponseDto } from 'src/dtos/download.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { DownloadService } from 'src/services/download.service';
import { assetStub } from 'test/fixtures/asset.stub';
Expand All @@ -25,17 +26,62 @@ describe(DownloadService.name, () => {
let sut: DownloadService;
let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let storageMock: Mocked<IStorageRepository>;

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

beforeEach(() => {
({ sut, accessMock, assetMock, storageMock } = newTestService(DownloadService));
({ sut, accessMock, assetMock, loggerMock, storageMock } = newTestService(DownloadService));
});

describe('downloadArchive', () => {
it('should skip asset ids that could not be found', async () => {
const archiveMock = {
addFile: vitest.fn(),
finalize: vitest.fn(),
stream: new Readable(),
};

accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
assetMock.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]);
storageMock.createZipStream.mockReturnValue(archiveMock);

await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
stream: archiveMock.stream,
});

expect(archiveMock.addFile).toHaveBeenCalledTimes(1);
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
});

it('should log a warning if the original path could not be resolved', async () => {
const archiveMock = {
addFile: vitest.fn(),
finalize: vitest.fn(),
stream: new Readable(),
};

accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
storageMock.realpath.mockRejectedValue(new Error('Could not read file'));
assetMock.getByIds.mockResolvedValue([
{ ...assetStub.noResizePath, id: 'asset-1' },
{ ...assetStub.noWebpPath, id: 'asset-2' },
]);
storageMock.createZipStream.mockReturnValue(archiveMock);

await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
stream: archiveMock.stream,
});

expect(loggerMock.warn).toHaveBeenCalledTimes(2);
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg');
});

it('should download an archive', async () => {
const archiveMock = {
addFile: vitest.fn(),
Expand Down
10 changes: 10 additions & 0 deletions server/src/services/duplicate.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
import { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { newTestService } from 'test/utils';
import { Mocked, beforeEach, vitest } from 'vitest';

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

describe('getDuplicates', () => {
it('should get duplicates', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
{ duplicateId: assetStub.hasDupe.duplicateId, assets: [expect.objectContaining({ id: assetStub.hasDupe.id })] },
]);
});
});

describe('handleQueueSearchDuplicates', () => {
beforeEach(() => {
systemMock.get.mockResolvedValue({
Expand Down
63 changes: 62 additions & 1 deletion server/src/services/map.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { MapService } from 'src/services/map.service';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { partnerStub } from 'test/fixtures/partner.stub';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

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

let albumMock: Mocked<IAlbumRepository>;
let mapMock: Mocked<IMapRepository>;
let partnerMock: Mocked<IPartnerRepository>;

beforeEach(() => {
({ sut, mapMock, partnerMock } = newTestService(MapService));
({ sut, albumMock, mapMock, partnerMock } = newTestService(MapService));
});

describe('getMapMarkers', () => {
Expand All @@ -35,5 +39,62 @@ describe(MapService.name, () => {
expect(markers).toHaveLength(1);
expect(markers[0]).toEqual(marker);
});

it('should include partner assets', async () => {
const asset = assetStub.withLocation;
const marker = {
id: asset.id,
lat: asset.exifInfo!.latitude!,
lon: asset.exifInfo!.longitude!,
city: asset.exifInfo!.city,
state: asset.exifInfo!.state,
country: asset.exifInfo!.country,
};
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
mapMock.getMapMarkers.mockResolvedValue([marker]);

const markers = await sut.getMapMarkers(authStub.user1, { withPartners: true });

expect(mapMock.getMapMarkers).toHaveBeenCalledWith(
[authStub.user1.user.id, partnerStub.adminToUser1.sharedById],
expect.arrayContaining([]),
{ withPartners: true },
);
expect(markers).toHaveLength(1);
expect(markers[0]).toEqual(marker);
});

it('should include assets from shared albums', async () => {
const asset = assetStub.withLocation;
const marker = {
id: asset.id,
lat: asset.exifInfo!.latitude!,
lon: asset.exifInfo!.longitude!,
city: asset.exifInfo!.city,
state: asset.exifInfo!.state,
country: asset.exifInfo!.country,
};
partnerMock.getAll.mockResolvedValue([]);
mapMock.getMapMarkers.mockResolvedValue([marker]);
albumMock.getOwned.mockResolvedValue([albumStub.empty]);
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);

const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true });

expect(markers).toHaveLength(1);
expect(markers[0]).toEqual(marker);
});
});

describe('reverseGeocode', () => {
it('should reverse geocode a location', async () => {
mapMock.reverseGeocode.mockResolvedValue({ city: 'foo', state: 'bar', country: 'baz' });

await expect(sut.reverseGeocode({ lat: 42, lon: 69 })).resolves.toEqual([
{ city: 'foo', state: 'bar', country: 'baz' },
]);

expect(mapMock.reverseGeocode).toHaveBeenCalledWith({ latitude: 42, longitude: 69 });
});
});
});
20 changes: 20 additions & 0 deletions server/src/services/notification.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ describe(NotificationService.name, () => {
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
});

it('should fail if smtp configuration is invalid', async () => {
const oldConfig = configs.smtpDisabled;
const newConfig = configs.smtpEnabled;

notificationMock.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error);
});
});

describe('onAssetHide', () => {
Expand Down Expand Up @@ -180,6 +188,18 @@ describe(NotificationService.name, () => {
});
});

describe('onSessionDeleteEvent', () => {
it('should send a on_session_delete client event', () => {
vi.useFakeTimers();
sut.onSessionDelete({ sessionId: 'id' });
expect(eventMock.clientSend).not.toHaveBeenCalled();

vi.advanceTimersByTime(500);

expect(eventMock.clientSend).toHaveBeenCalledWith('on_session_delete', 'id', 'id');
});
});

describe('onAssetTrash', () => {
it('should send connected clients an event', () => {
sut.onAssetTrash({ assetId: 'asset-id', userId: 'user-id' });
Expand Down
25 changes: 24 additions & 1 deletion server/src/services/partner.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.int
import { PartnerService } from 'src/services/partner.service';
import { authStub } from 'test/fixtures/auth.stub';
import { partnerStub } from 'test/fixtures/partner.stub';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

describe(PartnerService.name, () => {
let sut: PartnerService;

let accessMock: IAccessRepositoryMock;
let partnerMock: Mocked<IPartnerRepository>;

beforeEach(() => {
({ sut, partnerMock } = newTestService(PartnerService));
({ sut, accessMock, partnerMock } = newTestService(PartnerService));
});

it('should work', () => {
Expand Down Expand Up @@ -71,4 +74,24 @@ describe(PartnerService.name, () => {
expect(partnerMock.remove).not.toHaveBeenCalled();
});
});

describe('update', () => {
it('should require access', async () => {
await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: false })).rejects.toBeInstanceOf(
BadRequestException,
);
});

it('should update partner', async () => {
accessMock.partner.checkUpdateAccess.mockResolvedValue(new Set(['shared-by-id']));
partnerMock.update.mockResolvedValue(partnerStub.adminToUser1);

await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: true })).resolves.toBeDefined();
expect(partnerMock.update).toHaveBeenCalledWith({
sharedById: 'shared-by-id',
sharedWithId: authStub.admin.user.id,
inTimeline: true,
});
});
});
});
21 changes: 20 additions & 1 deletion server/src/services/shared-link.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,21 @@ describe(SharedLinkService.name, () => {
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});

it('should throw an error for an password protected shared link', async () => {
it('should throw an error for an invalid password protected shared link', async () => {
const authDto = authStub.adminSharedLink;
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException);
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});

it('should allow a correct password on a password protected shared link', async () => {
sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined();
expect(sharedLinkMock.get).toHaveBeenCalledWith(
authStub.adminSharedLink.user.id,
authStub.adminSharedLink.sharedLink?.id,
);
});
});

describe('get', () => {
Expand Down Expand Up @@ -300,5 +309,15 @@ describe(SharedLinkService.name, () => {
});
expect(sharedLinkMock.get).toHaveBeenCalled();
});

it('should return metadata tags with a default image path if the asset id is not set', async () => {
sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '0 shared photos & videos',
imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/feature-panel.png`,
title: 'Public Share',
});
expect(sharedLinkMock.get).toHaveBeenCalled();
});
});
});
2 changes: 1 addition & 1 deletion server/src/services/shared-link.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { OpenGraphTags } from 'src/utils/misc';

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

Expand Down
Loading

0 comments on commit 9d0f038

Please sign in to comment.