Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: finishing unit tests for a couple of services #13292

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading