feat: medium tests for user and sync service (#16304)
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { GenericContainer, Wait } from 'testcontainers';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const globalSetup = async () => {
|
||||
const postgres = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
|
||||
.withExposedPorts(5432)
|
||||
.withEnvironment({
|
||||
POSTGRES_PASSWORD: 'postgres',
|
||||
POSTGRES_USER: 'postgres',
|
||||
POSTGRES_DB: 'immich',
|
||||
})
|
||||
.withCommand([
|
||||
'postgres',
|
||||
'-c',
|
||||
'shared_preload_libraries=vectors.so',
|
||||
'-c',
|
||||
'search_path="$$user", public, vectors',
|
||||
'-c',
|
||||
'max_wal_size=2GB',
|
||||
'-c',
|
||||
'shared_buffers=512MB',
|
||||
'-c',
|
||||
'fsync=off',
|
||||
'-c',
|
||||
'full_page_writes=off',
|
||||
'-c',
|
||||
'synchronous_commit=off',
|
||||
])
|
||||
.withWaitStrategy(Wait.forAll([Wait.forLogMessage('database system is ready to accept connections', 2)]))
|
||||
.start();
|
||||
|
||||
const postgresPort = postgres.getMappedPort(5432);
|
||||
const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`;
|
||||
process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const modules = import.meta.glob('/src/migrations/*.ts', { eager: true });
|
||||
|
||||
const config = {
|
||||
type: 'postgres' as const,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
migrations: Object.values(modules).map((module) => Object.values(module)[0]),
|
||||
migrationsRun: false,
|
||||
synchronize: false,
|
||||
connectTimeoutMS: 10_000, // 10 seconds
|
||||
parseInt8: true,
|
||||
url: postgresUrl,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const dataSource = new DataSource(config);
|
||||
await dataSource.initialize();
|
||||
await dataSource.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
||||
await dataSource.runMigrations();
|
||||
await dataSource.destroy();
|
||||
};
|
||||
|
||||
export default globalSetup;
|
||||
@@ -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',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user