mirror of
https://github.com/immich-app/immich.git
synced 2025-12-24 09:14:58 +03:00
@@ -1,11 +1,12 @@
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { randomBytes, randomUUID } from 'node:crypto';
|
||||
import { Writable } from 'node:stream';
|
||||
import { Assets, DB, Sessions, Users } from 'src/db';
|
||||
import { Assets, DB, Partners, Sessions, Users } from 'src/db';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
@@ -30,6 +31,7 @@ class CustomWritable extends Writable {
|
||||
type Asset = Insertable<Assets>;
|
||||
type User = Partial<Insertable<Users>>;
|
||||
type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string };
|
||||
type Partner = Insertable<Partners>;
|
||||
|
||||
export const newUuid = () => randomUUID() as string;
|
||||
|
||||
@@ -37,6 +39,7 @@ export class TestFactory {
|
||||
private assets: Asset[] = [];
|
||||
private sessions: Session[] = [];
|
||||
private users: User[] = [];
|
||||
private partners: Partner[] = [];
|
||||
|
||||
private constructor(private context: TestContext) {}
|
||||
|
||||
@@ -100,6 +103,17 @@ export class TestFactory {
|
||||
};
|
||||
}
|
||||
|
||||
static partner(partner: Partner) {
|
||||
const defaults = {
|
||||
inTimeline: true,
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...partner,
|
||||
};
|
||||
}
|
||||
|
||||
withAsset(asset: Asset) {
|
||||
this.assets.push(asset);
|
||||
return this;
|
||||
@@ -115,6 +129,11 @@ export class TestFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
withPartner(partner: Partner) {
|
||||
this.partners.push(partner);
|
||||
return this;
|
||||
}
|
||||
|
||||
async create() {
|
||||
for (const asset of this.assets) {
|
||||
await this.context.createAsset(asset);
|
||||
@@ -124,6 +143,10 @@ export class TestFactory {
|
||||
await this.context.createUser(user);
|
||||
}
|
||||
|
||||
for (const partner of this.partners) {
|
||||
await this.context.createPartner(partner);
|
||||
}
|
||||
|
||||
for (const session of this.sessions) {
|
||||
await this.context.createSession(session);
|
||||
}
|
||||
@@ -138,6 +161,7 @@ export class TestContext {
|
||||
albumRepository: AlbumRepository;
|
||||
sessionRepository: SessionRepository;
|
||||
syncRepository: SyncRepository;
|
||||
partnerRepository: PartnerRepository;
|
||||
|
||||
private constructor(private db: Kysely<DB>) {
|
||||
this.userRepository = new UserRepository(this.db);
|
||||
@@ -145,6 +169,7 @@ export class TestContext {
|
||||
this.albumRepository = new AlbumRepository(this.db);
|
||||
this.sessionRepository = new SessionRepository(this.db);
|
||||
this.syncRepository = new SyncRepository(this.db);
|
||||
this.partnerRepository = new PartnerRepository(this.db);
|
||||
}
|
||||
|
||||
static from(db: Kysely<DB>) {
|
||||
@@ -159,6 +184,10 @@ export class TestContext {
|
||||
return this.userRepository.create(TestFactory.user(user));
|
||||
}
|
||||
|
||||
createPartner(partner: Partner) {
|
||||
return this.partnerRepository.create(TestFactory.partner(partner));
|
||||
}
|
||||
|
||||
createAsset(asset: Asset) {
|
||||
return this.assetRepository.create(TestFactory.asset(asset));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ const setup = async () => {
|
||||
|
||||
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
||||
const stream = TestFactory.stream();
|
||||
// Wait for 1ms to ensure all updates are available
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
await sut.stream(auth, stream, { types });
|
||||
|
||||
return stream.getResponse();
|
||||
@@ -186,4 +188,178 @@ describe(SyncService.name, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent('partners', () => {
|
||||
it('should detect and sync the first partner', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
|
||||
const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
|
||||
const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
await context.partnerRepository.remove(partner);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerDeleteV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a partner share both to and from another user', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
|
||||
const partner1 = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const partner2 = await context.createPartner({ sharedById: user1.id, sharedWithId: user2.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner1.inTimeline,
|
||||
sharedById: partner1.sharedById,
|
||||
sharedWithId: partner1.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner2.inTimeline,
|
||||
sharedById: partner2.sharedById,
|
||||
sharedWithId: partner2.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await sut.setAcks(auth, { acks: [response[1].ack] });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sync a partner and then an update to that same partner', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
|
||||
const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const updated = await context.partnerRepository.update(
|
||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||
{ inTimeline: true },
|
||||
);
|
||||
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: updated.inTimeline,
|
||||
sharedById: updated.sharedById,
|
||||
sharedWithId: updated.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not sync a partner for an unrelated user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const user3 = await context.createUser();
|
||||
|
||||
await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,5 +9,7 @@ export const newSyncRepositoryMock = (): Mocked<RepositoryInterface<SyncReposito
|
||||
deleteCheckpoints: vitest.fn(),
|
||||
getUserUpserts: vitest.fn(),
|
||||
getUserDeletes: vitest.fn(),
|
||||
getPartnerUpserts: vitest.fn(),
|
||||
getPartnerDeletes: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user