feat: medium tests for user and sync service (#16304)

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
Jason Rasmussen 2025-02-25 11:31:07 -05:00 committed by GitHub
parent ae61ea7984
commit 7c851893b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 634 additions and 24 deletions

View file

@ -0,0 +1,135 @@
import { Stats } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { AssetEntity } from 'src/entities/asset.entity';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service';
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newRandomImage, newTestService, ServiceMocks } from 'test/utils';
const metadataRepository = new MetadataRepository(
newLoggingRepositoryMock() as ILoggingRepository as LoggingRepository,
);
const createTestFile = async (exifData: Record<string, any>) => {
const data = newRandomImage();
const filePath = join(tmpdir(), 'test.png');
await writeFile(filePath, data);
await metadataRepository.writeTags(filePath, exifData);
return { filePath };
};
type TimeZoneTest = {
description: string;
serverTimeZone?: string;
exifData: Record<string, any>;
expected: {
localDateTime: string;
dateTimeOriginal: string;
timeZone: string | null;
};
};
describe(MetadataService.name, () => {
let sut: MetadataService;
let mocks: ServiceMocks;
beforeEach(() => {
({ sut, mocks } = newTestService(MetadataService, { metadataRepository }));
mocks.storage.stat.mockResolvedValue({ size: 123_456 } as Stats);
delete process.env.TZ;
});
it('should be defined', () => {
expect(sut).toBeDefined();
});
describe('handleMetadataExtraction', () => {
const timeZoneTests: TimeZoneTest[] = [
{
description: 'should handle no time zone information',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
dateTimeOriginal: '2022-01-01T00:00:00.000Z',
timeZone: null,
},
},
{
description: 'should handle no time zone information and server behind UTC',
serverTimeZone: 'America/Los_Angeles',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
dateTimeOriginal: '2022-01-01T08:00:00.000Z',
timeZone: null,
},
},
{
description: 'should handle no time zone information and server ahead of UTC',
serverTimeZone: 'Europe/Brussels',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
dateTimeOriginal: '2021-12-31T23:00:00.000Z',
timeZone: null,
},
},
{
description: 'should handle no time zone information and server ahead of UTC in the summer',
serverTimeZone: 'Europe/Brussels',
exifData: {
DateTimeOriginal: '2022:06:01 00:00:00',
},
expected: {
localDateTime: '2022-06-01T00:00:00.000Z',
dateTimeOriginal: '2022-05-31T22:00:00.000Z',
timeZone: null,
},
},
{
description: 'should handle a +13:00 time zone',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00+13:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
dateTimeOriginal: '2021-12-31T11:00:00.000Z',
timeZone: 'UTC+13',
},
},
];
it.each(timeZoneTests)('$description', async ({ exifData, serverTimeZone, expected }) => {
process.env.TZ = serverTimeZone ?? undefined;
const { filePath } = await createTestFile(exifData);
mocks.asset.getByIds.mockResolvedValue([{ id: 'asset-1', originalPath: filePath } as AssetEntity]);
await sut.handleMetadataExtraction({ id: 'asset-1' });
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
expect.objectContaining({
dateTimeOriginal: new Date(expected.dateTimeOriginal),
timeZone: expected.timeZone,
}),
);
expect(mocks.asset.update).toHaveBeenCalledWith(
expect.objectContaining({
localDateTime: new Date(expected.localDateTime),
}),
);
});
});
});

View file

@ -0,0 +1,189 @@
import { AuthDto } from 'src/dtos/auth.dto';
import { SyncRequestType } from 'src/enum';
import { SyncService } from 'src/services/sync.service';
import { TestContext, TestFactory } from 'test/factory';
import { getKyselyDB, newTestService } from 'test/utils';
const setup = async () => {
const user = TestFactory.user();
const session = TestFactory.session({ userId: user.id });
const auth = TestFactory.auth({ session, user });
const db = await getKyselyDB();
const context = await TestContext.from(db).withUser(user).withSession(session).create();
const { sut } = newTestService(SyncService, context);
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
const stream = TestFactory.stream();
await sut.stream(auth, stream, { types });
return stream.getResponse();
};
return {
auth,
context,
sut,
testSync,
};
};
describe(SyncService.name, () => {
describe.concurrent('users', () => {
it('should detect and sync the first user', async () => {
const { context, auth, sut, testSync } = await setup();
const user = await context.userRepository.get(auth.user.id, { withDeleted: false });
if (!user) {
expect.fail('First user should exist');
}
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(initialSyncResponse).toHaveLength(1);
expect(initialSyncResponse).toEqual([
{
ack: expect.any(String),
data: {
deletedAt: user.deletedAt,
email: user.email,
id: user.id,
name: user.name,
},
type: 'UserV1',
},
]);
const acks = [initialSyncResponse[0].ack];
await sut.setAcks(auth, { acks });
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(ackSyncResponse).toHaveLength(0);
});
it('should detect and sync a soft deleted user', async () => {
const { auth, context, sut, testSync } = await setup();
const deletedAt = new Date().toISOString();
const deleted = await context.createUser({ deletedAt });
const response = await testSync(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(2);
expect(response).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
data: {
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: auth.user.name,
},
type: 'UserV1',
},
{
ack: expect.any(String),
data: {
deletedAt,
email: deleted.email,
id: deleted.id,
name: deleted.name,
},
type: 'UserV1',
},
]),
);
const acks = [response[1].ack];
await sut.setAcks(auth, { acks });
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(ackSyncResponse).toHaveLength(0);
});
it('should detect and sync a deleted user', async () => {
const { auth, context, sut, testSync } = await setup();
const user = await context.createUser();
await context.userRepository.delete({ id: user.id }, true);
const response = await testSync(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(2);
expect(response).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
data: {
userId: user.id,
},
type: 'UserDeleteV1',
},
{
ack: expect.any(String),
data: {
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: auth.user.name,
},
type: 'UserV1',
},
]),
);
const acks = response.map(({ ack }) => ack);
await sut.setAcks(auth, { acks });
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(ackSyncResponse).toHaveLength(0);
});
it('should sync a user and then an update to that same user', async () => {
const { auth, context, sut, testSync } = await setup();
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(initialSyncResponse).toHaveLength(1);
expect(initialSyncResponse).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
data: {
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: auth.user.name,
},
type: 'UserV1',
},
]),
);
const acks = [initialSyncResponse[0].ack];
await sut.setAcks(auth, { acks });
const updated = await context.userRepository.update(auth.user.id, { name: 'new name' });
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(updatedSyncResponse).toHaveLength(1);
expect(updatedSyncResponse).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
data: {
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: updated.name,
},
type: 'UserV1',
},
]),
);
});
});
});

View file

@ -0,0 +1,116 @@
import { UserService } from 'src/services/user.service';
import { TestContext, TestFactory } from 'test/factory';
import { getKyselyDB, newTestService } from 'test/utils';
describe.concurrent(UserService.name, () => {
let sut: UserService;
let context: TestContext;
beforeAll(async () => {
const db = await getKyselyDB();
context = await TestContext.from(db).withUser({ isAdmin: true }).create();
({ sut } = newTestService(UserService, context));
});
describe('create', () => {
it('should create a user', async () => {
const userDto = TestFactory.user();
await expect(sut.createUser(userDto)).resolves.toEqual(
expect.objectContaining({
id: userDto.id,
name: userDto.name,
email: userDto.email,
}),
);
});
it('should reject user with duplicate email', async () => {
const userDto = TestFactory.user();
const userDto2 = TestFactory.user({ email: userDto.email });
await sut.createUser(userDto);
await expect(sut.createUser(userDto2)).rejects.toThrow('User exists');
});
it('should not return password', async () => {
const user = await sut.createUser(TestFactory.user());
expect((user as any).password).toBeUndefined();
});
});
describe('get', () => {
it('should get a user', async () => {
const userDto = TestFactory.user();
await context.createUser(userDto);
await expect(sut.get(userDto.id)).resolves.toEqual(
expect.objectContaining({
id: userDto.id,
name: userDto.name,
email: userDto.email,
}),
);
});
it('should not return password', async () => {
const { id } = await context.createUser();
const user = await sut.get(id);
expect((user as any).password).toBeUndefined();
});
});
describe('updateMe', () => {
it('should update a user', async () => {
const userDto = TestFactory.user();
const sessionDto = TestFactory.session({ userId: userDto.id });
const authDto = TestFactory.auth({ user: userDto });
const before = await context.createUser(userDto);
await context.createSession(sessionDto);
const newUserDto = TestFactory.user();
const after = await sut.updateMe(authDto, { name: newUserDto.name, email: newUserDto.email });
if (!before || !after) {
expect.fail('User should be found');
}
expect(before.updatedAt).toBeDefined();
expect(after.updatedAt).toBeDefined();
expect(before.updatedAt).not.toEqual(after.updatedAt);
expect(after).toEqual(expect.objectContaining({ name: newUserDto.name, email: newUserDto.email }));
});
});
describe('setLicense', () => {
const userLicense = {
licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4',
activationKey:
'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw',
};
it('should set a license', async () => {
const userDto = TestFactory.user();
const sessionDto = TestFactory.session({ userId: userDto.id });
const authDto = TestFactory.auth({ user: userDto });
await context.getFactory().withUser(userDto).withSession(sessionDto).create();
await expect(sut.getLicense(authDto)).rejects.toThrowError();
const after = await sut.setLicense(authDto, userLicense);
expect(after.licenseKey).toEqual(userLicense.licenseKey);
expect(after.activationKey).toEqual(userLicense.activationKey);
const getResponse = await sut.getLicense(authDto);
expect(getResponse).toEqual(after);
});
});
});