diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 1a86b870e1..1b4ca91f3b 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -69,6 +69,7 @@ class SyncEntityType { static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); static const syncResetV1 = SyncEntityType._(r'SyncResetV1'); + static const syncCompleteV1 = SyncEntityType._(r'SyncCompleteV1'); /// List of all possible values in this [enum][SyncEntityType]. static const values = [ @@ -118,6 +119,7 @@ class SyncEntityType { userMetadataDeleteV1, syncAckV1, syncResetV1, + syncCompleteV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -202,6 +204,7 @@ class SyncEntityTypeTypeTransformer { case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; case r'SyncAckV1': return SyncEntityType.syncAckV1; case r'SyncResetV1': return SyncEntityType.syncResetV1; + case r'SyncCompleteV1': return SyncEntityType.syncCompleteV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e2b2aa1905..a03c2be1e7 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -15416,6 +15416,10 @@ ], "type": "object" }, + "SyncCompleteV1": { + "properties": {}, + "type": "object" + }, "SyncEntityType": { "enum": [ "AuthUserV1", @@ -15463,7 +15467,8 @@ "UserMetadataV1", "UserMetadataDeleteV1", "SyncAckV1", - "SyncResetV1" + "SyncResetV1", + "SyncCompleteV1" ], "type": "string" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0f0357c32d..d26e2f0524 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -4921,7 +4921,8 @@ export enum SyncEntityType { UserMetadataV1 = "UserMetadataV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1", SyncAckV1 = "SyncAckV1", - SyncResetV1 = "SyncResetV1" + SyncResetV1 = "SyncResetV1", + SyncCompleteV1 = "SyncCompleteV1" } export enum SyncRequestType { AlbumsV1 = "AlbumsV1", diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 0fae619e0f..c936ec52cc 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -336,6 +336,9 @@ export class SyncAckV1 {} @ExtraModel() export class SyncResetV1 {} +@ExtraModel() +export class SyncCompleteV1 {} + export type SyncItem = { [SyncEntityType.AuthUserV1]: SyncAuthUserV1; [SyncEntityType.UserV1]: SyncUserV1; @@ -382,6 +385,7 @@ export type SyncItem = { [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; [SyncEntityType.SyncAckV1]: SyncAckV1; + [SyncEntityType.SyncCompleteV1]: SyncCompleteV1; [SyncEntityType.SyncResetV1]: SyncResetV1; }; diff --git a/server/src/enum.ts b/server/src/enum.ts index bf72b24a14..9f04c4a9ee 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -530,6 +530,7 @@ export enum JobName { AssetGenerateThumbnails = 'AssetGenerateThumbnails', AuditLogCleanup = 'AuditLogCleanup', + AuditTableCleanup = 'AuditTableCleanup', DatabaseBackup = 'DatabaseBackup', @@ -708,6 +709,7 @@ export enum SyncEntityType { SyncAckV1 = 'SyncAckV1', SyncResetV1 = 'SyncResetV1', + SyncCompleteV1 = 'SyncCompleteV1', } export enum NotificationLevel { diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 3e70baa5d4..2a1b9d1631 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -957,7 +957,7 @@ where order by "stack"."updateId" asc --- SyncRepository.people.getDeletes +-- SyncRepository.person.getDeletes select "id", "personId" @@ -970,7 +970,7 @@ where order by "person_audit"."id" asc --- SyncRepository.people.getUpserts +-- SyncRepository.person.getUpserts select "id", "createdAt", diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 398d49bd5d..6917921008 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Kysely } from 'kysely'; +import { Kysely, sql } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -62,7 +62,7 @@ export class SyncRepository { partnerAsset: PartnerAssetsSync; partnerAssetExif: PartnerAssetExifsSync; partnerStack: PartnerStackSync; - people: PersonSync; + person: PersonSync; stack: StackSync; user: UserSync; userMetadata: UserMetadataSync; @@ -84,7 +84,7 @@ export class SyncRepository { this.partnerAsset = new PartnerAssetsSync(this.db); this.partnerAssetExif = new PartnerAssetExifsSync(this.db); this.partnerStack = new PartnerStackSync(this.db); - this.people = new PersonSync(this.db); + this.person = new PersonSync(this.db); this.stack = new StackSync(this.db); this.user = new UserSync(this.db); this.userMetadata = new UserMetadataSync(this.db); @@ -117,6 +117,15 @@ class BaseSync { .orderBy(idRef, 'asc'); } + protected auditCleanup(t: T, days: number) { + const { table, ref } = this.db.dynamic; + + return this.db + .deleteFrom(table(t).as(t)) + .where(ref(`${t}.deletedAt`), '<', sql.raw(`now() - interval '${days} days'`)) + .execute(); + } + protected upsertQuery(t: T, { nowId, ack }: SyncQueryOptions) { const { table, ref } = this.db.dynamic; const updateIdRef = ref(`${t}.updateId`); @@ -150,6 +159,10 @@ class AlbumSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('album_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -286,6 +299,10 @@ class AlbumToAssetSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('album_asset_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -334,6 +351,10 @@ class AlbumUserSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('album_user_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -371,6 +392,10 @@ class AssetSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('asset', options) @@ -400,6 +425,10 @@ class PersonSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('person_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('person', options) @@ -431,6 +460,10 @@ class AssetFaceSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_face_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('asset_face', options) @@ -473,6 +506,10 @@ class MemorySync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('memory_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('memory', options) @@ -505,6 +542,10 @@ class MemoryToAssetSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('memory_asset_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('memory_asset', options) @@ -537,6 +578,10 @@ class PartnerSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('partner_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { const userId = options.userId; @@ -616,6 +661,10 @@ class StackSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('stack_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('stack', options) @@ -664,6 +713,10 @@ class UserSync extends BaseSync { return this.auditQuery('user_audit', options).select(['id', 'userId']).stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('user_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('user', options).select(columns.syncUser).stream(); @@ -679,6 +732,10 @@ class UserMetadataSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('user_metadata_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions], stream: true }) getUpserts(options: SyncQueryOptions) { return this.upsertQuery('user_metadata', options) @@ -698,6 +755,10 @@ class AssetMetadataSync extends BaseSync { .stream(); } + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_metadata_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) getUpserts(options: SyncQueryOptions, userId: string) { return this.upsertQuery('asset_metadata', options) diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 48f454d455..c8474cda03 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -166,6 +166,7 @@ export interface DB { api_key: ApiKeyTable; asset: AssetTable; + asset_audit: AssetAuditTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; asset_face_audit: AssetFaceAuditTable; @@ -173,7 +174,6 @@ export interface DB { asset_metadata: AssetMetadataTable; asset_metadata_audit: AssetMetadataAuditTable; asset_job_status: AssetJobStatusTable; - asset_audit: AssetAuditTable; audit: AuditTable; diff --git a/server/src/schema/tables/memory-asset-audit.table.ts b/server/src/schema/tables/memory-asset-audit.table.ts index 77a889b455..218c2f19ff 100644 --- a/server/src/schema/tables/memory-asset-audit.table.ts +++ b/server/src/schema/tables/memory-asset-audit.table.ts @@ -1,11 +1,11 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { MemoryTable } from 'src/schema/tables/memory.table'; -import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('memory_asset_audit') export class MemoryAssetAuditTable { @PrimaryGeneratedUuidV7Column() - id!: string; + id!: Generated; @ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' }) memoryId!: string; @@ -14,5 +14,5 @@ export class MemoryAssetAuditTable { assetId!: string; @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) - deletedAt!: Date; + deletedAt!: Generated; } diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 63d5fb2d06..1ff34ed35a 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -42,6 +42,7 @@ describe(JobService.name, () => { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.AuditTableCleanup }, { name: JobName.AuditLogCleanup }, { name: JobName.MemoryGenerate }, { name: JobName.UserSyncUsage }, diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 0116c869c6..89de4879c5 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -281,6 +281,7 @@ export class JobService extends BaseService { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.AuditTableCleanup }, { name: JobName.AuditLogCleanup }, ); } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 677c799fb8..f354a71791 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,8 +1,9 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; -import { DateTime } from 'luxon'; +import { DateTime, Duration } from 'luxon'; import { Writable } from 'node:stream'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; +import { OnJob } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { @@ -15,7 +16,16 @@ import { SyncItem, SyncStreamDto, } from 'src/dtos/sync.dto'; -import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum'; +import { + AssetVisibility, + DatabaseAction, + EntityType, + JobName, + Permission, + QueueName, + SyncEntityType, + SyncRequestType, +} from 'src/enum'; import { SyncQueryOptions } from 'src/repositories/sync.repository'; import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table'; import { BaseService } from 'src/services/base.service'; @@ -32,6 +42,8 @@ type AssetLike = Omit & { }; const COMPLETE_ID = 'complete'; +const MAX_DAYS = 30; +const MAX_DURATION = Duration.fromObject({ days: MAX_DAYS }); const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({ ...data, @@ -137,19 +149,24 @@ export class SyncService extends BaseService { } const isPendingSyncReset = await this.sessionRepository.isPendingSyncReset(session.id); - if (isPendingSyncReset) { send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} }); response.end(); return; } + const checkpoints = await this.syncCheckpointRepository.getAll(session.id); + const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); + + if (this.needsFullSync(checkpointMap)) { + send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} }); + response.end(); + return; + } + const { nowId } = await this.syncCheckpointRepository.getNow(); const options: SyncQueryOptions = { nowId, userId: auth.user.id }; - const checkpoints = await this.syncCheckpointRepository.getAll(session.id); - const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); - const handlers: Record Promise> = { [SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap), [SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap), @@ -180,9 +197,41 @@ export class SyncService extends BaseService { await handler(); } + send(response, { type: SyncEntityType.SyncCompleteV1, ids: [nowId], data: {} }); + response.end(); } + @OnJob({ name: JobName.AuditTableCleanup, queue: QueueName.BackgroundTask }) + async onAuditTableCleanup() { + const pruneThreshold = MAX_DAYS + 1; + + await this.syncRepository.album.cleanupAuditTable(pruneThreshold); + await this.syncRepository.albumUser.cleanupAuditTable(pruneThreshold); + await this.syncRepository.albumToAsset.cleanupAuditTable(pruneThreshold); + await this.syncRepository.asset.cleanupAuditTable(pruneThreshold); + await this.syncRepository.assetFace.cleanupAuditTable(pruneThreshold); + await this.syncRepository.assetMetadata.cleanupAuditTable(pruneThreshold); + await this.syncRepository.memory.cleanupAuditTable(pruneThreshold); + await this.syncRepository.memoryToAsset.cleanupAuditTable(pruneThreshold); + await this.syncRepository.partner.cleanupAuditTable(pruneThreshold); + await this.syncRepository.person.cleanupAuditTable(pruneThreshold); + await this.syncRepository.stack.cleanupAuditTable(pruneThreshold); + await this.syncRepository.user.cleanupAuditTable(pruneThreshold); + await this.syncRepository.userMetadata.cleanupAuditTable(pruneThreshold); + } + + private needsFullSync(checkpointMap: CheckpointMap) { + const completeAck = checkpointMap[SyncEntityType.SyncCompleteV1]; + if (!completeAck) { + return false; + } + + const milliseconds = Number.parseInt(completeAck.updateId.replaceAll('-', '').slice(0, 12), 16); + + return DateTime.fromMillis(milliseconds) < DateTime.now().minus(MAX_DURATION); + } + private async syncAuthUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { const upsertType = SyncEntityType.AuthUserV1; const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] }); @@ -719,13 +768,13 @@ export class SyncService extends BaseService { private async syncPeopleV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { const deleteType = SyncEntityType.PersonDeleteV1; - const deletes = this.syncRepository.people.getDeletes({ ...options, ack: checkpointMap[deleteType] }); + const deletes = this.syncRepository.person.getDeletes({ ...options, ack: checkpointMap[deleteType] }); for await (const { id, ...data } of deletes) { send(response, { type: deleteType, ids: [id], data }); } const upsertType = SyncEntityType.PersonV1; - const upserts = this.syncRepository.people.getUpserts({ ...options, ack: checkpointMap[upsertType] }); + const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { send(response, { type: upsertType, ids: [updateId], data }); } diff --git a/server/src/types.ts b/server/src/types.ts index 2b603aeec5..0cd0df63f4 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -275,6 +275,9 @@ export interface QueueStatus { } export type JobItem = + // Audit + | { name: JobName.AuditTableCleanup; data?: IBaseJob } + // Backups | { name: JobName.DatabaseBackup; data?: IBaseJob } diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 066996ead5..dd1f98d6eb 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -35,7 +35,7 @@ export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif primaryAssetId: assets[0].id, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), - updateId: 'uuid-v7', + updateId: expect.any(String), }; }; diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index 7a2cacf126..ca66af7b94 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,5 +1,6 @@ import { Tag } from 'src/database'; import { TagResponseDto } from 'src/dtos/tag.dto'; +import { newUuidV7 } from 'test/small.factory'; const parent = Object.freeze({ id: 'tag-parent', @@ -37,7 +38,10 @@ const color = { parentId: null, }; -const upsert = { userId: 'tag-user', updateId: 'uuid-v7' }; +const upsert = { + userId: 'tag-user', + updateId: newUuidV7(), +}; export const tagStub = { tag, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 87c8406f55..a169d96322 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -258,6 +258,12 @@ export class SyncTestContext extends MediumTestContext { return stream.getResponse(); } + async assertSyncIsComplete(auth: AuthDto, types: SyncRequestType[]) { + await expect(this.syncStream(auth, types)).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + } + async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) { const acks: Record = {}; const syncAcks: string[] = []; diff --git a/server/test/medium/specs/services/sync.service.spec.ts b/server/test/medium/specs/services/sync.service.spec.ts new file mode 100644 index 0000000000..b5443d7e62 --- /dev/null +++ b/server/test/medium/specs/services/sync.service.spec.ts @@ -0,0 +1,226 @@ +import { Kysely } from 'kysely'; +import { DateTime } from 'luxon'; +import { AssetMetadataKey, UserMetadataKey } from 'src/enum'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { SyncRepository } from 'src/repositories/sync.repository'; +import { DB } from 'src/schema'; +import { SyncService } from 'src/services/sync.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; +import { v4 } from 'uuid'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(SyncService, { + database: db || defaultDatabase, + real: [DatabaseRepository, SyncRepository], + mock: [LoggingRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +const deletedLongAgo = DateTime.now().minus({ days: 35 }).toISO(); + +const assertTableCount = async (db: Kysely, t: T, count: number) => { + const { table } = db.dynamic; + const results = await db.selectFrom(table(t).as(t)).selectAll().execute(); + expect(results).toHaveLength(count); +}; + +describe(SyncService.name, () => { + describe('onAuditTableCleanup', () => { + it('should work', async () => { + const { sut } = setup(); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + }); + + it('should cleanup the album_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'album_audit'; + + await ctx.database + .insertInto(tableName) + .values({ albumId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the album_asset_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'album_asset_audit'; + const { user } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + await ctx.database + .insertInto(tableName) + .values({ albumId: album.id, assetId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the album_user_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'album_user_audit'; + await ctx.database + .insertInto(tableName) + .values({ albumId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the asset_audit table', async () => { + const { sut, ctx } = setup(); + + await ctx.database + .insertInto('asset_audit') + .values({ assetId: v4(), ownerId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, 'asset_audit', 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, 'asset_audit', 0); + }); + + it('should cleanup the asset_face_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'asset_face_audit'; + await ctx.database + .insertInto(tableName) + .values({ assetFaceId: v4(), assetId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the asset_metadata_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'asset_metadata_audit'; + await ctx.database + .insertInto(tableName) + .values({ assetId: v4(), key: AssetMetadataKey.MobileApp, deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the memory_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'memory_audit'; + await ctx.database + .insertInto(tableName) + .values({ memoryId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the memory_asset_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'memory_asset_audit'; + const { user } = await ctx.newUser(); + const { memory } = await ctx.newMemory({ ownerId: user.id }); + await ctx.database + .insertInto(tableName) + .values({ memoryId: memory.id, assetId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the partner_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'partner_audit'; + await ctx.database + .insertInto(tableName) + .values({ sharedById: v4(), sharedWithId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the stack_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'stack_audit'; + await ctx.database + .insertInto(tableName) + .values({ stackId: v4(), userId: v4(), deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the user_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'user_audit'; + await ctx.database.insertInto(tableName).values({ userId: v4(), deletedAt: deletedLongAgo }).execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should cleanup the user_metadata_audit table', async () => { + const { sut, ctx } = setup(); + const tableName = 'user_metadata_audit'; + await ctx.database + .insertInto(tableName) + .values({ userId: v4(), key: UserMetadataKey.Onboarding, deletedAt: deletedLongAgo }) + .execute(); + + await assertTableCount(ctx.database, tableName, 1); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + await assertTableCount(ctx.database, tableName, 0); + }); + + it('should skip recent records', async () => { + const { sut, ctx } = setup(); + + const keep = { + id: v4(), + assetId: v4(), + ownerId: v4(), + deletedAt: DateTime.now().minus({ days: 25 }).toISO(), + }; + + const remove = { + id: v4(), + assetId: v4(), + ownerId: v4(), + deletedAt: DateTime.now().minus({ days: 35 }).toISO(), + }; + + await ctx.database.insertInto('asset_audit').values([keep, remove]).execute(); + await assertTableCount(ctx.database, 'asset_audit', 2); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + + const after = await ctx.database.selectFrom('asset_audit').select(['id']).execute(); + expect(after).toHaveLength(1); + expect(after[0].id).toBe(keep.id); + }); + }); +}); diff --git a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts index 9e994604a5..fd563f4db1 100644 --- a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts @@ -74,11 +74,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }, type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(2); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should sync album asset exif for own user', async () => { @@ -88,8 +88,15 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(2); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.SyncAckV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumAssetExifCreateV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); }); it('should not sync album asset exif for unrelated user', async () => { @@ -104,8 +111,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should backfill album assets exif when a user shares an album with you', async () => { @@ -139,8 +149,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(2); // ack initial album asset exif sync await ctx.syncAckAll(auth, response); @@ -174,11 +184,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(newResponse).toHaveLength(5); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should sync old asset exif when a user adds them to an album they share you', async () => { @@ -207,8 +217,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(firstAlbumResponse).toHaveLength(2); await ctx.syncAckAll(auth, firstAlbumResponse); @@ -224,8 +234,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { type: SyncEntityType.AlbumAssetExifBackfillV1, }, backfillSyncAck, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(2); // ack initial album asset sync await ctx.syncAckAll(auth, response); @@ -244,11 +254,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(newResponse).toHaveLength(2); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetExifsV1]); }); it('should sync asset exif updates for an album shared with you', async () => { @@ -262,7 +272,6 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -272,6 +281,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -283,9 +293,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { city: 'New City', }); - const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(updateResponse).toHaveLength(1); - expect(updateResponse).toEqual([ + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -294,6 +302,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifUpdateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); @@ -330,8 +339,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(response).toHaveLength(3); await ctx.syncAckAll(auth, response); @@ -342,8 +351,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { city: 'Delayed Exif', }); - const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(updateResponse).toEqual([ + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -352,7 +360,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { }), type: SyncEntityType.AlbumAssetExifUpdateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); - expect(updateResponse).toHaveLength(1); }); }); diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index cbc60a2c5a..4f053937b8 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -58,7 +58,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -83,10 +82,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }, type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should sync album asset for own user', async () => { @@ -95,8 +95,15 @@ describe(SyncRequestType.AlbumAssetsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(2); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.SyncAckV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumAssetCreateV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); }); it('should not sync album asset for unrelated user', async () => { @@ -110,8 +117,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should backfill album assets when a user shares an album with you', async () => { @@ -133,7 +143,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -143,6 +152,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial album asset sync @@ -176,10 +186,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should sync old assets when a user adds them to an album they share you', async () => { @@ -196,7 +207,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(firstAlbumResponse).toHaveLength(2); expect(firstAlbumResponse).toEqual([ updateSyncAck, { @@ -206,6 +216,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, firstAlbumResponse); @@ -213,7 +224,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - // expect(response).toHaveLength(2); expect(response).toEqual([ { ack: expect.any(String), @@ -223,6 +233,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { type: SyncEntityType.AlbumAssetBackfillV1, }, backfillSyncAck, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial album asset sync @@ -242,10 +253,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]); }); it('should sync asset updates for an album shared with you', async () => { @@ -258,7 +270,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ updateSyncAck, { @@ -268,6 +279,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetCreateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -280,7 +292,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }); const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); - expect(updateResponse).toHaveLength(1); expect(updateResponse).toEqual([ { ack: expect.any(String), @@ -290,6 +301,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }), type: SyncEntityType.AlbumAssetUpdateV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index ee529c5001..b6bd9db010 100644 --- a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts @@ -28,7 +28,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -38,10 +37,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should sync album to asset for owned albums', async () => { @@ -51,7 +51,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -61,10 +60,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should detect and sync the album to asset for shared albums', async () => { @@ -76,7 +76,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -86,10 +85,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should not sync album to asset for an album owned by another user', async () => { @@ -98,7 +98,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should backfill album to assets when a user shares an album with you', async () => { @@ -114,7 +114,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -124,6 +123,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial album to asset sync @@ -148,10 +148,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should detect and sync a deleted album to asset relation', async () => { @@ -162,7 +163,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -172,6 +172,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -179,7 +180,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await wait(2); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -189,10 +189,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should detect and sync a deleted album to asset relation when an asset is deleted', async () => { @@ -203,7 +204,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -213,6 +213,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -220,7 +221,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await wait(2); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -230,10 +230,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); it('should not sync a deleted album to asset relation when the album is deleted', async () => { @@ -244,7 +245,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -254,11 +254,12 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { }, type: SyncEntityType.AlbumToAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await albumRepo.delete(album.id); await wait(2); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-user.spec.ts b/server/test/medium/specs/sync/sync-album-user.spec.ts index e3d8a21493..d779ffd9f3 100644 --- a/server/test/medium/specs/sync/sync-album-user.spec.ts +++ b/server/test/medium/specs/sync/sync-album-user.spec.ts @@ -34,6 +34,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); @@ -45,7 +46,6 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -56,10 +56,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync an updated shared user', async () => { @@ -71,11 +72,10 @@ describe(SyncRequestType.AlbumUsersV1, () => { const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.Viewer }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -86,10 +86,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync a deleted shared user', async () => { @@ -100,9 +101,8 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -115,10 +115,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); }); @@ -134,7 +135,6 @@ describe(SyncRequestType.AlbumUsersV1, () => { }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -145,10 +145,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync an updated shared user', async () => { @@ -161,10 +162,14 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(2); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.Viewer }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -178,10 +183,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should detect and sync a deleted shared user', async () => { @@ -194,10 +200,14 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(2); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); await albumUserRepo.delete({ albumsId: album.id, usersId: user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -210,10 +220,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); it('should backfill album users when a user shares an album with you', async () => { @@ -232,7 +243,6 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -243,6 +253,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); // ack initial user @@ -285,10 +296,11 @@ describe(SyncRequestType.AlbumUsersV1, () => { }), type: SyncEntityType.AlbumUserV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-album.spec.ts b/server/test/medium/specs/sync/sync-album.spec.ts index 9f44e617e3..591d7e1f3c 100644 --- a/server/test/medium/specs/sync/sync-album.spec.ts +++ b/server/test/medium/specs/sync/sync-album.spec.ts @@ -24,7 +24,6 @@ describe(SyncRequestType.AlbumsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -35,10 +34,11 @@ describe(SyncRequestType.AlbumsV1, () => { }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync a new album', async () => { @@ -46,7 +46,6 @@ describe(SyncRequestType.AlbumsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -55,10 +54,11 @@ describe(SyncRequestType.AlbumsV1, () => { }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album delete', async () => { @@ -67,7 +67,6 @@ describe(SyncRequestType.AlbumsV1, () => { const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -76,12 +75,12 @@ describe(SyncRequestType.AlbumsV1, () => { }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await albumRepo.delete(album.id); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -90,10 +89,11 @@ describe(SyncRequestType.AlbumsV1, () => { }, type: SyncEntityType.AlbumDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); describe('shared albums', () => { @@ -104,17 +104,17 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: album.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album share (share before sync)', async () => { @@ -124,17 +124,17 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: album.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album share (share after sync)', async () => { @@ -150,23 +150,24 @@ describe(SyncRequestType.AlbumsV1, () => { data: expect.objectContaining({ id: userAlbum.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.Editor }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user2Album.id }), type: SyncEntityType.AlbumV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album delete`', async () => { @@ -177,24 +178,27 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); await albumRepo.delete(album.id); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: { albumId: album.id }, type: SyncEntityType.AlbumDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); it('should detect and sync an album unshare as an album delete', async () => { @@ -205,10 +209,13 @@ describe(SyncRequestType.AlbumsV1, () => { await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); - expect(response).toHaveLength(1); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.AlbumV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); @@ -218,10 +225,11 @@ describe(SyncRequestType.AlbumsV1, () => { data: { albumId: album.id }, type: SyncEntityType.AlbumDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-asset-exif.spec.ts index 425ea89054..9aae961b0c 100644 --- a/server/test/medium/specs/sync/sync-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-exif.spec.ts @@ -24,7 +24,6 @@ describe(SyncRequestType.AssetExifsV1, () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -57,10 +56,11 @@ describe(SyncRequestType.AssetExifsV1, () => { }, type: SyncEntityType.AssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetExifsV1]); }); it('should only sync asset exif for own user', async () => { @@ -72,7 +72,10 @@ describe(SyncRequestType.AssetExifsV1, () => { const { session } = await ctx.newSession({ userId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetExifsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-face.spec.ts b/server/test/medium/specs/sync/sync-asset-face.spec.ts index 68d3007c52..8b4310e600 100644 --- a/server/test/medium/specs/sync/sync-asset-face.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-face.spec.ts @@ -26,7 +26,6 @@ describe(SyncEntityType.AssetFaceV1, () => { const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -44,10 +43,11 @@ describe(SyncEntityType.AssetFaceV1, () => { }), type: 'AssetFaceV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); it('should detect and sync a deleted asset face', async () => { @@ -58,7 +58,6 @@ describe(SyncEntityType.AssetFaceV1, () => { await personRepo.deleteAssetFace(assetFace.id); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -67,10 +66,11 @@ describe(SyncEntityType.AssetFaceV1, () => { }, type: 'AssetFaceDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); it('should not sync an asset face or asset face delete for an unrelated user', async () => { @@ -82,11 +82,18 @@ describe(SyncEntityType.AssetFaceV1, () => { const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); await personRepo.deleteAssetFace(assetFace.id); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-metadata.spec.ts b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts index 84353883a2..8ba9630520 100644 --- a/server/test/medium/specs/sync/sync-asset-metadata.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts @@ -26,7 +26,6 @@ describe(SyncEntityType.AssetMetadataV1, () => { await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -37,10 +36,11 @@ describe(SyncEntityType.AssetMetadataV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetMetadataV1]); }); it('should update asset metadata', async () => { @@ -51,7 +51,6 @@ describe(SyncEntityType.AssetMetadataV1, () => { await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -62,6 +61,7 @@ describe(SyncEntityType.AssetMetadataV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -79,10 +79,11 @@ describe(SyncEntityType.AssetMetadataV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, updatedResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetMetadataV1]); }); }); @@ -95,7 +96,6 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -106,6 +106,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { }, type: 'AssetMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -121,6 +122,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { }, type: 'AssetMetadataDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index ce83eed98c..066cb2de4d 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -40,7 +40,6 @@ describe(SyncEntityType.AssetV1, () => { }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -64,10 +63,11 @@ describe(SyncEntityType.AssetV1, () => { }, type: 'AssetV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); it('should detect and sync a deleted asset', async () => { @@ -77,7 +77,6 @@ describe(SyncEntityType.AssetV1, () => { await assetRepo.remove(asset); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -86,10 +85,11 @@ describe(SyncEntityType.AssetV1, () => { }, type: 'AssetDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); it('should not sync an asset or asset delete for an unrelated user', async () => { @@ -100,11 +100,17 @@ describe(SyncEntityType.AssetV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); await assetRepo.remove(asset); - expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-auth-user.spec.ts b/server/test/medium/specs/sync/sync-auth-user.spec.ts index 80ce8b37fa..eef18e957d 100644 --- a/server/test/medium/specs/sync/sync-auth-user.spec.ts +++ b/server/test/medium/specs/sync/sync-auth-user.spec.ts @@ -22,7 +22,6 @@ describe(SyncEntityType.AuthUserV1, () => { const { auth, user, ctx } = await setup(await getKyselyDB()); const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -43,10 +42,11 @@ describe(SyncEntityType.AuthUserV1, () => { }, type: 'AuthUserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.AuthUsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AuthUsersV1]); }); it('should sync a change and then another change to that same user', async () => { @@ -55,7 +55,6 @@ describe(SyncEntityType.AuthUserV1, () => { const userRepo = ctx.get(UserRepository); const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -65,6 +64,7 @@ describe(SyncEntityType.AuthUserV1, () => { }), type: 'AuthUserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -72,7 +72,6 @@ describe(SyncEntityType.AuthUserV1, () => { await userRepo.update(user.id, { isAdmin: true }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -82,6 +81,7 @@ describe(SyncEntityType.AuthUserV1, () => { }), type: 'AuthUserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-complete.spec.ts b/server/test/medium/specs/sync/sync-complete.spec.ts new file mode 100644 index 0000000000..8a94061631 --- /dev/null +++ b/server/test/medium/specs/sync/sync-complete.spec.ts @@ -0,0 +1,60 @@ +import { Kysely } from 'kysely'; +import { DateTime } from 'luxon'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository'; +import { DB } from 'src/schema'; +import { toAck } from 'src/utils/sync'; +import { SyncTestContext } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; +import { v7 } from 'uuid'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.SyncCompleteV1, () => { + it('should work', async () => { + const { auth, ctx } = await setup(); + + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); + }); + + it('should detect an old checkpoint and send back a reset', async () => { + const { auth, session, ctx } = await setup(); + const updateId = v7({ msecs: DateTime.now().minus({ days: 60 }).toMillis() }); + + await ctx.get(SyncCheckpointRepository).upsertAll([ + { + type: SyncEntityType.SyncCompleteV1, + sessionId: session.id, + ack: toAck({ type: SyncEntityType.SyncCompleteV1, updateId }), + }, + ]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); + expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' }]); + }); + + it('should not send back a reset if the checkpoint is recent', async () => { + const { auth, session, ctx } = await setup(); + const updateId = v7({ msecs: DateTime.now().minus({ days: 7 }).toMillis() }); + + await ctx.get(SyncCheckpointRepository).upsertAll([ + { + type: SyncEntityType.SyncCompleteV1, + sessionId: session.id, + ack: toAck({ type: SyncEntityType.SyncCompleteV1, updateId }), + }, + ]); + + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); + }); +}); diff --git a/server/test/medium/specs/sync/sync-memory-asset.spec.ts b/server/test/medium/specs/sync/sync-memory-asset.spec.ts index a3247637d7..f0cae0934e 100644 --- a/server/test/medium/specs/sync/sync-memory-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-memory-asset.spec.ts @@ -25,7 +25,6 @@ describe(SyncEntityType.MemoryToAssetV1, () => { await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -35,10 +34,11 @@ describe(SyncEntityType.MemoryToAssetV1, () => { }, type: 'MemoryToAssetV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); }); it('should detect and sync a deleted memory to asset relation', async () => { @@ -50,7 +50,6 @@ describe(SyncEntityType.MemoryToAssetV1, () => { await memoryRepo.removeAssetIds(memory.id, [asset.id]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -60,10 +59,11 @@ describe(SyncEntityType.MemoryToAssetV1, () => { }, type: 'MemoryToAssetDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); }); it('should not sync a memory to asset relation or delete for an unrelated user', async () => { @@ -74,11 +74,18 @@ describe(SyncEntityType.MemoryToAssetV1, () => { const { memory } = await ctx.newMemory({ ownerId: user2.id }); await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); - expect(await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(0); - expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1); + expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.MemoryToAssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); await memoryRepo.removeAssetIds(memory.id, [asset.id]); - expect(await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(0); - expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1); + + expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.MemoryToAssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-memory.spec.ts b/server/test/medium/specs/sync/sync-memory.spec.ts index fa833ad094..1889f39626 100644 --- a/server/test/medium/specs/sync/sync-memory.spec.ts +++ b/server/test/medium/specs/sync/sync-memory.spec.ts @@ -23,7 +23,6 @@ describe(SyncEntityType.MemoryV1, () => { const { memory } = await ctx.newMemory({ ownerId: user1.id }); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -43,10 +42,11 @@ describe(SyncEntityType.MemoryV1, () => { }, type: 'MemoryV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); it('should detect and sync a deleted memory', async () => { @@ -56,7 +56,6 @@ describe(SyncEntityType.MemoryV1, () => { await memoryRepo.delete(memory.id); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -65,10 +64,11 @@ describe(SyncEntityType.MemoryV1, () => { }, type: 'MemoryDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); it('should sync a memory and then an update to that same memory', async () => { @@ -77,29 +77,29 @@ describe(SyncEntityType.MemoryV1, () => { const { memory } = await ctx.newMemory({ ownerId: user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: memory.id }), type: 'MemoryV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await memoryRepo.update(memory.id, { seenAt: new Date() }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: memory.id }), type: 'MemoryV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); it('should not sync a memory or a memory delete for an unrelated user', async () => { @@ -108,8 +108,8 @@ describe(SyncEntityType.MemoryV1, () => { const { user: user2 } = await ctx.newUser(); const { memory } = await ctx.newMemory({ ownerId: user2.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); await memoryRepo.delete(memory.id); - await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts index c33eb59dbb..d44c088f17 100644 --- a/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts @@ -26,7 +26,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -59,10 +58,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }, type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should not sync partner asset exif for own user', async () => { @@ -72,8 +72,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); await ctx.newExif({ assetId: asset.id, make: 'Canon' }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should not sync partner asset exif for unrelated user', async () => { @@ -86,8 +89,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetExifV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should backfill partner asset exif when a partner shared their library with you', async () => { @@ -102,7 +108,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual( expect.arrayContaining([ { @@ -112,6 +117,7 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]), ); @@ -119,7 +125,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -133,10 +138,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should handle partners with users ids lower than a uuidv7', async () => { @@ -151,7 +157,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -160,15 +165,15 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); // This checks that our ack upsert is correct - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), @@ -182,10 +187,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { @@ -203,7 +209,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -212,13 +217,13 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(newResponse).toHaveLength(3); expect(newResponse).toEqual([ { ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), @@ -239,9 +244,10 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { }), type: SyncEntityType.PartnerAssetExifV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index e9dc7403bd..c30cfcf6bd 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -46,7 +46,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -70,10 +69,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }, type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should detect and sync a deleted partner asset', async () => { @@ -86,7 +86,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await assetRepo.remove(asset); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -95,10 +94,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }, type: SyncEntityType.PartnerAssetDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync a deleted partner asset due to a user delete', async () => { @@ -109,7 +109,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await ctx.newAsset({ ownerId: user2.id }); await userRepo.delete({ id: user2.id }, true); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { @@ -119,9 +119,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => { const { user: user2 } = await ctx.newUser(); await ctx.newAsset({ ownerId: user2.id }); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.PartnerAssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await partnerRepo.remove(partner); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync an asset or asset delete for own user', async () => { @@ -132,13 +135,19 @@ describe(SyncRequestType.PartnerAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); await assetRepo.remove(asset); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should not sync an asset or asset delete for unrelated user', async () => { @@ -150,13 +159,19 @@ describe(SyncRequestType.PartnerAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); await assetRepo.remove(asset); - await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should backfill partner assets when a partner shared their library with you', async () => { @@ -170,7 +185,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -179,13 +193,13 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }), type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -199,10 +213,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { @@ -218,7 +233,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -227,12 +241,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }), type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); - expect(newResponse).toHaveLength(3); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -253,9 +267,10 @@ describe(SyncRequestType.PartnerAssetsV1, () => { }), type: SyncEntityType.PartnerAssetV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-stack.spec.ts b/server/test/medium/specs/sync/sync-partner-stack.spec.ts index 3a879cb580..e1d8416799 100644 --- a/server/test/medium/specs/sync/sync-partner-stack.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-stack.spec.ts @@ -29,7 +29,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -42,10 +41,11 @@ describe(SyncRequestType.PartnerStacksV1, () => { }, type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should detect and sync a deleted partner stack', async () => { @@ -58,7 +58,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { await stackRepo.delete(stack.id); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('PartnerStackDeleteV1'), @@ -67,10 +66,11 @@ describe(SyncRequestType.PartnerStacksV1, () => { }, type: SyncEntityType.PartnerStackDeleteV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a deleted partner stack due to a user delete', async () => { @@ -81,7 +81,7 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); await ctx.newStack({ ownerId: user2.id }, [asset.id]); await userRepo.delete({ id: user2.id }, true); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a deleted partner stack due to a partner delete (unshare)', async () => { @@ -91,9 +91,12 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { asset } = await ctx.newAsset({ ownerId: user2.id }); await ctx.newStack({ ownerId: user2.id }, [asset.id]); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.PartnerStackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await partnerRepo.remove(partner); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a stack or stack delete for own user', async () => { @@ -103,11 +106,17 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { asset } = await ctx.newAsset({ ownerId: user.id }); const { stack } = await ctx.newStack({ ownerId: user.id }, [asset.id]); await ctx.newPartner({ sharedById: user2.id, sharedWithId: user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); await stackRepo.delete(stack.id); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should not sync a stack or stack delete for unrelated user', async () => { @@ -119,13 +128,19 @@ describe(SyncRequestType.PartnerStacksV1, () => { const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]); const auth2 = factory.auth({ session, user: user2 }); - await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); await stackRepo.delete(stack.id); - await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.StackDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should backfill partner stacks when a partner shared their library with you', async () => { @@ -140,7 +155,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('PartnerStackV1'), @@ -149,12 +163,12 @@ describe(SyncRequestType.PartnerStacksV1, () => { }), type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(newResponse).toHaveLength(2); expect(newResponse).toEqual([ { ack: expect.stringContaining(SyncEntityType.PartnerStackBackfillV1), @@ -168,10 +182,11 @@ describe(SyncRequestType.PartnerStacksV1, () => { data: {}, type: SyncEntityType.SyncAckV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); it('should only backfill partner stacks created prior to the current partner stack checkpoint', async () => { @@ -189,7 +204,6 @@ describe(SyncRequestType.PartnerStacksV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining(SyncEntityType.PartnerStackV1), @@ -198,12 +212,12 @@ describe(SyncRequestType.PartnerStacksV1, () => { }), type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); - expect(newResponse).toHaveLength(3); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -224,9 +238,10 @@ describe(SyncRequestType.PartnerStacksV1, () => { }), type: SyncEntityType.PartnerStackV1, }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner.spec.ts b/server/test/medium/specs/sync/sync-partner.spec.ts index d20970da8f..19c386070a 100644 --- a/server/test/medium/specs/sync/sync-partner.spec.ts +++ b/server/test/medium/specs/sync/sync-partner.spec.ts @@ -26,7 +26,6 @@ describe(SyncEntityType.PartnerV1, () => { const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -37,10 +36,11 @@ describe(SyncEntityType.PartnerV1, () => { }, type: 'PartnerV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should detect and sync a deleted partner', async () => { @@ -53,22 +53,20 @@ describe(SyncEntityType.PartnerV1, () => { await partnerRepo.remove(partner); const response = await ctx.syncStream(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', + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, }, - ]), - ); + type: 'PartnerDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should detect and sync a partner share both to and from another user', async () => { @@ -79,32 +77,30 @@ describe(SyncEntityType.PartnerV1, () => { const { partner: partner2 } = await ctx.newPartner({ sharedById: user1.id, sharedWithId: user2.id }); const response = await ctx.syncStream(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', + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + inTimeline: partner1.inTimeline, + sharedById: partner1.sharedById, + sharedWithId: partner1.sharedWithId, }, - { - ack: expect.any(String), - data: { - inTimeline: partner2.inTimeline, - sharedById: partner2.sharedById, - sharedWithId: partner2.sharedWithId, - }, - type: 'PartnerV1', + type: 'PartnerV1', + }, + { + ack: expect.any(String), + data: { + inTimeline: partner2.inTimeline, + sharedById: partner2.sharedById, + sharedWithId: partner2.sharedWithId, }, - ]), - ); + type: 'PartnerV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should sync a partner and then an update to that same partner', async () => { @@ -116,7 +112,6 @@ describe(SyncEntityType.PartnerV1, () => { const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -127,6 +122,7 @@ describe(SyncEntityType.PartnerV1, () => { }, type: 'PartnerV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -137,7 +133,6 @@ describe(SyncEntityType.PartnerV1, () => { ); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), @@ -148,10 +143,11 @@ describe(SyncEntityType.PartnerV1, () => { }, type: 'PartnerV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should not sync a partner or partner delete for an unrelated user', async () => { @@ -163,9 +159,9 @@ describe(SyncEntityType.PartnerV1, () => { const { user: user3 } = await ctx.newUser(); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user3.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); await partnerRepo.remove(partner); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); it('should not sync a partner delete after a user is deleted', async () => { @@ -177,6 +173,6 @@ describe(SyncEntityType.PartnerV1, () => { await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await userRepo.delete({ id: user2.id }, true); - await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-person.spec.ts b/server/test/medium/specs/sync/sync-person.spec.ts index fbf401e377..6fdb5a58f2 100644 --- a/server/test/medium/specs/sync/sync-person.spec.ts +++ b/server/test/medium/specs/sync/sync-person.spec.ts @@ -24,7 +24,6 @@ describe(SyncEntityType.PersonV1, () => { const { person } = await ctx.newPerson({ ownerId: auth.user.id }); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -40,10 +39,11 @@ describe(SyncEntityType.PersonV1, () => { }), type: 'PersonV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PeopleV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); }); it('should detect and sync a deleted person', async () => { @@ -53,7 +53,6 @@ describe(SyncEntityType.PersonV1, () => { await personRepo.delete([person.id]); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -62,10 +61,11 @@ describe(SyncEntityType.PersonV1, () => { }, type: 'PersonDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.PeopleV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); }); it('should not sync a person or person delete for an unrelated user', async () => { @@ -76,11 +76,18 @@ describe(SyncEntityType.PersonV1, () => { const { person } = await ctx.newPerson({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.PersonV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); await personRepo.delete([person.id]); - expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); - expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); + + expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toEqual([ + expect.objectContaining({ type: SyncEntityType.PersonDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-reset.spec.ts b/server/test/medium/specs/sync/sync-reset.spec.ts index 699c5dc292..9a4c33c1f2 100644 --- a/server/test/medium/specs/sync/sync-reset.spec.ts +++ b/server/test/medium/specs/sync/sync-reset.spec.ts @@ -21,8 +21,7 @@ describe(SyncEntityType.SyncResetV1, () => { it('should work', async () => { const { auth, ctx } = await setup(); - const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); - expect(response).toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]); }); it('should detect a pending sync reset', async () => { @@ -41,7 +40,10 @@ describe(SyncEntityType.SyncResetV1, () => { await ctx.newAsset({ ownerId: user.id }); - await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.get(SessionRepository).update(auth.session!.id, { isPendingSyncReset: true, @@ -62,9 +64,8 @@ describe(SyncEntityType.SyncResetV1, () => { }); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([ - expect.objectContaining({ - type: SyncEntityType.AssetV1, - }), + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); @@ -86,9 +87,8 @@ describe(SyncEntityType.SyncResetV1, () => { const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); expect(postResetResponse).toEqual([ - expect.objectContaining({ - type: SyncEntityType.AssetV1, - }), + expect.objectContaining({ type: SyncEntityType.AssetV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-stack.spec.ts b/server/test/medium/specs/sync/sync-stack.spec.ts index 1696172911..d3304ded28 100644 --- a/server/test/medium/specs/sync/sync-stack.spec.ts +++ b/server/test/medium/specs/sync/sync-stack.spec.ts @@ -25,7 +25,6 @@ describe(SyncEntityType.StackV1, () => { const { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('StackV1'), @@ -38,10 +37,11 @@ describe(SyncEntityType.StackV1, () => { }, type: 'StackV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); it('should detect and sync a deleted stack', async () => { @@ -53,17 +53,17 @@ describe(SyncEntityType.StackV1, () => { await stackRepo.delete(stack.id); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.stringContaining('StackDeleteV1'), data: { stackId: stack.id }, type: 'StackDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); it('should sync a stack and then an update to that same stack', async () => { @@ -74,22 +74,29 @@ describe(SyncEntityType.StackV1, () => { const { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(response).toHaveLength(1); + expect(response).toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); await ctx.syncAckAll(auth, response); await stackRepo.update(stack.id, { primaryAssetId: asset2.id }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); - expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ + expect.objectContaining({ type: SyncEntityType.StackV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); expect(newResponse).toEqual([ { ack: expect.stringContaining('StackV1'), data: expect.objectContaining({ id: stack.id, primaryAssetId: asset2.id }), type: 'StackV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, newResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); it('should not sync a stack or stack delete for an unrelated user', async () => { @@ -100,8 +107,8 @@ describe(SyncEntityType.StackV1, () => { const { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id }); const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset1.id, asset2.id]); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); await stackRepo.delete(stack.id); - await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]); }); }); diff --git a/server/test/medium/specs/sync/sync-user-metadata.spec.ts b/server/test/medium/specs/sync/sync-user-metadata.spec.ts index 7cd53e76e3..1e75f80194 100644 --- a/server/test/medium/specs/sync/sync-user-metadata.spec.ts +++ b/server/test/medium/specs/sync/sync-user-metadata.spec.ts @@ -25,7 +25,6 @@ describe(SyncEntityType.UserMetadataV1, () => { await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -36,10 +35,11 @@ describe(SyncEntityType.UserMetadataV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UserMetadataV1]); }); it('should update user metadata', async () => { @@ -49,7 +49,6 @@ describe(SyncEntityType.UserMetadataV1, () => { await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -60,6 +59,7 @@ describe(SyncEntityType.UserMetadataV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -77,10 +77,11 @@ describe(SyncEntityType.UserMetadataV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, updatedResponse); - await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UserMetadataV1]); }); }); @@ -92,7 +93,6 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -103,6 +103,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { }, type: 'UserMetadataV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -118,6 +119,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { }, type: 'UserMetadataDeleteV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/medium/specs/sync/sync-user.spec.ts b/server/test/medium/specs/sync/sync-user.spec.ts index c5d572d7d6..7a69e7a411 100644 --- a/server/test/medium/specs/sync/sync-user.spec.ts +++ b/server/test/medium/specs/sync/sync-user.spec.ts @@ -28,7 +28,6 @@ describe(SyncEntityType.UserV1, () => { } const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), @@ -43,10 +42,11 @@ describe(SyncEntityType.UserV1, () => { }, type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UsersV1]); }); it('should detect and sync a soft deleted user', async () => { @@ -56,7 +56,6 @@ describe(SyncEntityType.UserV1, () => { const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(2); expect(response).toEqual( expect.arrayContaining([ { @@ -69,11 +68,12 @@ describe(SyncEntityType.UserV1, () => { data: expect.objectContaining({ id: deleted.id }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]), ); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UsersV1]); }); it('should detect and sync a deleted user', async () => { @@ -85,7 +85,6 @@ describe(SyncEntityType.UserV1, () => { await userRepo.delete({ id: user.id }, true); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(2); expect(response).toEqual([ { ack: expect.any(String), @@ -99,10 +98,11 @@ describe(SyncEntityType.UserV1, () => { data: expect.objectContaining({ id: authUser.id }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); - await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.UsersV1]); }); it('should sync a user and then an update to that same user', async () => { @@ -111,13 +111,13 @@ describe(SyncEntityType.UserV1, () => { const userRepo = ctx.get(UserRepository); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(response).toHaveLength(1); expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user.id }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); await ctx.syncAckAll(auth, response); @@ -125,13 +125,13 @@ describe(SyncEntityType.UserV1, () => { const updated = await userRepo.update(auth.user.id, { name: 'new name' }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); - expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user.id, name: updated.name }), type: 'UserV1', }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), ]); }); }); diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 8b44b6eddc..04654552a3 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -1,4 +1,3 @@ -import { randomUUID } from 'node:crypto'; import { Activity, ApiKey, @@ -17,14 +16,15 @@ import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { OnThisDayData, UserMetadataItem } from 'src/types'; +import { v4, v7 } from 'uuid'; -export const newUuid = () => randomUUID() as string; +export const newUuid = () => v4(); export const newUuids = () => Array.from({ length: 100 }) .fill(0) .map(() => newUuid()); export const newDate = () => new Date(); -export const newUuidV7 = () => 'uuid-v7'; +export const newUuidV7 = () => v7(); export const newSha1 = () => Buffer.from('this is a fake hash'); export const newEmbedding = () => { const embedding = Array.from({ length: 512 })