feat: audit cleanup (#21567)

This commit is contained in:
Jason Rasmussen
2025-09-03 18:50:27 -04:00
committed by GitHub
parent af1e18d07e
commit 28179a3a1d
40 changed files with 839 additions and 299 deletions

View File

@@ -69,6 +69,7 @@ class SyncEntityType {
static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1');
static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
static const syncResetV1 = SyncEntityType._(r'SyncResetV1'); static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
static const syncCompleteV1 = SyncEntityType._(r'SyncCompleteV1');
/// List of all possible values in this [enum][SyncEntityType]. /// List of all possible values in this [enum][SyncEntityType].
static const values = <SyncEntityType>[ static const values = <SyncEntityType>[
@@ -118,6 +119,7 @@ class SyncEntityType {
userMetadataDeleteV1, userMetadataDeleteV1,
syncAckV1, syncAckV1,
syncResetV1, syncResetV1,
syncCompleteV1,
]; ];
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
@@ -202,6 +204,7 @@ class SyncEntityTypeTypeTransformer {
case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1;
case r'SyncAckV1': return SyncEntityType.syncAckV1; case r'SyncAckV1': return SyncEntityType.syncAckV1;
case r'SyncResetV1': return SyncEntityType.syncResetV1; case r'SyncResetV1': return SyncEntityType.syncResetV1;
case r'SyncCompleteV1': return SyncEntityType.syncCompleteV1;
default: default:
if (!allowNull) { if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data'); throw ArgumentError('Unknown enum value to decode: $data');

View File

@@ -15416,6 +15416,10 @@
], ],
"type": "object" "type": "object"
}, },
"SyncCompleteV1": {
"properties": {},
"type": "object"
},
"SyncEntityType": { "SyncEntityType": {
"enum": [ "enum": [
"AuthUserV1", "AuthUserV1",
@@ -15463,7 +15467,8 @@
"UserMetadataV1", "UserMetadataV1",
"UserMetadataDeleteV1", "UserMetadataDeleteV1",
"SyncAckV1", "SyncAckV1",
"SyncResetV1" "SyncResetV1",
"SyncCompleteV1"
], ],
"type": "string" "type": "string"
}, },

View File

@@ -4921,7 +4921,8 @@ export enum SyncEntityType {
UserMetadataV1 = "UserMetadataV1", UserMetadataV1 = "UserMetadataV1",
UserMetadataDeleteV1 = "UserMetadataDeleteV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1",
SyncAckV1 = "SyncAckV1", SyncAckV1 = "SyncAckV1",
SyncResetV1 = "SyncResetV1" SyncResetV1 = "SyncResetV1",
SyncCompleteV1 = "SyncCompleteV1"
} }
export enum SyncRequestType { export enum SyncRequestType {
AlbumsV1 = "AlbumsV1", AlbumsV1 = "AlbumsV1",

View File

@@ -336,6 +336,9 @@ export class SyncAckV1 {}
@ExtraModel() @ExtraModel()
export class SyncResetV1 {} export class SyncResetV1 {}
@ExtraModel()
export class SyncCompleteV1 {}
export type SyncItem = { export type SyncItem = {
[SyncEntityType.AuthUserV1]: SyncAuthUserV1; [SyncEntityType.AuthUserV1]: SyncAuthUserV1;
[SyncEntityType.UserV1]: SyncUserV1; [SyncEntityType.UserV1]: SyncUserV1;
@@ -382,6 +385,7 @@ export type SyncItem = {
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
[SyncEntityType.SyncAckV1]: SyncAckV1; [SyncEntityType.SyncAckV1]: SyncAckV1;
[SyncEntityType.SyncCompleteV1]: SyncCompleteV1;
[SyncEntityType.SyncResetV1]: SyncResetV1; [SyncEntityType.SyncResetV1]: SyncResetV1;
}; };

View File

@@ -530,6 +530,7 @@ export enum JobName {
AssetGenerateThumbnails = 'AssetGenerateThumbnails', AssetGenerateThumbnails = 'AssetGenerateThumbnails',
AuditLogCleanup = 'AuditLogCleanup', AuditLogCleanup = 'AuditLogCleanup',
AuditTableCleanup = 'AuditTableCleanup',
DatabaseBackup = 'DatabaseBackup', DatabaseBackup = 'DatabaseBackup',
@@ -708,6 +709,7 @@ export enum SyncEntityType {
SyncAckV1 = 'SyncAckV1', SyncAckV1 = 'SyncAckV1',
SyncResetV1 = 'SyncResetV1', SyncResetV1 = 'SyncResetV1',
SyncCompleteV1 = 'SyncCompleteV1',
} }
export enum NotificationLevel { export enum NotificationLevel {

View File

@@ -957,7 +957,7 @@ where
order by order by
"stack"."updateId" asc "stack"."updateId" asc
-- SyncRepository.people.getDeletes -- SyncRepository.person.getDeletes
select select
"id", "id",
"personId" "personId"
@@ -970,7 +970,7 @@ where
order by order by
"person_audit"."id" asc "person_audit"."id" asc
-- SyncRepository.people.getUpserts -- SyncRepository.person.getUpserts
select select
"id", "id",
"createdAt", "createdAt",

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Kysely } from 'kysely'; import { Kysely, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database'; import { columns } from 'src/database';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
@@ -62,7 +62,7 @@ export class SyncRepository {
partnerAsset: PartnerAssetsSync; partnerAsset: PartnerAssetsSync;
partnerAssetExif: PartnerAssetExifsSync; partnerAssetExif: PartnerAssetExifsSync;
partnerStack: PartnerStackSync; partnerStack: PartnerStackSync;
people: PersonSync; person: PersonSync;
stack: StackSync; stack: StackSync;
user: UserSync; user: UserSync;
userMetadata: UserMetadataSync; userMetadata: UserMetadataSync;
@@ -84,7 +84,7 @@ export class SyncRepository {
this.partnerAsset = new PartnerAssetsSync(this.db); this.partnerAsset = new PartnerAssetsSync(this.db);
this.partnerAssetExif = new PartnerAssetExifsSync(this.db); this.partnerAssetExif = new PartnerAssetExifsSync(this.db);
this.partnerStack = new PartnerStackSync(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.stack = new StackSync(this.db);
this.user = new UserSync(this.db); this.user = new UserSync(this.db);
this.userMetadata = new UserMetadataSync(this.db); this.userMetadata = new UserMetadataSync(this.db);
@@ -117,6 +117,15 @@ class BaseSync {
.orderBy(idRef, 'asc'); .orderBy(idRef, 'asc');
} }
protected auditCleanup<T extends keyof DB>(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 extends keyof DB>(t: T, { nowId, ack }: SyncQueryOptions) { protected upsertQuery<T extends keyof DB>(t: T, { nowId, ack }: SyncQueryOptions) {
const { table, ref } = this.db.dynamic; const { table, ref } = this.db.dynamic;
const updateIdRef = ref(`${t}.updateId`); const updateIdRef = ref(`${t}.updateId`);
@@ -150,6 +159,10 @@ class AlbumSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('album_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
const userId = options.userId; const userId = options.userId;
@@ -286,6 +299,10 @@ class AlbumToAssetSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('album_asset_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
const userId = options.userId; const userId = options.userId;
@@ -334,6 +351,10 @@ class AlbumUserSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('album_user_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
const userId = options.userId; const userId = options.userId;
@@ -371,6 +392,10 @@ class AssetSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('asset_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('asset', options) return this.upsertQuery('asset', options)
@@ -400,6 +425,10 @@ class PersonSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('person_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('person', options) return this.upsertQuery('person', options)
@@ -431,6 +460,10 @@ class AssetFaceSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('asset_face_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('asset_face', options) return this.upsertQuery('asset_face', options)
@@ -473,6 +506,10 @@ class MemorySync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('memory_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('memory', options) return this.upsertQuery('memory', options)
@@ -505,6 +542,10 @@ class MemoryToAssetSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('memory_asset_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('memory_asset', options) return this.upsertQuery('memory_asset', options)
@@ -537,6 +578,10 @@ class PartnerSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('partner_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
const userId = options.userId; const userId = options.userId;
@@ -616,6 +661,10 @@ class StackSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('stack_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('stack', options) return this.upsertQuery('stack', options)
@@ -664,6 +713,10 @@ class UserSync extends BaseSync {
return this.auditQuery('user_audit', options).select(['id', 'userId']).stream(); return this.auditQuery('user_audit', options).select(['id', 'userId']).stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('user_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('user', options).select(columns.syncUser).stream(); return this.upsertQuery('user', options).select(columns.syncUser).stream();
@@ -679,6 +732,10 @@ class UserMetadataSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('user_metadata_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions], stream: true }) @GenerateSql({ params: [dummyQueryOptions], stream: true })
getUpserts(options: SyncQueryOptions) { getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('user_metadata', options) return this.upsertQuery('user_metadata', options)
@@ -698,6 +755,10 @@ class AssetMetadataSync extends BaseSync {
.stream(); .stream();
} }
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('asset_metadata_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
getUpserts(options: SyncQueryOptions, userId: string) { getUpserts(options: SyncQueryOptions, userId: string) {
return this.upsertQuery('asset_metadata', options) return this.upsertQuery('asset_metadata', options)

View File

@@ -166,6 +166,7 @@ export interface DB {
api_key: ApiKeyTable; api_key: ApiKeyTable;
asset: AssetTable; asset: AssetTable;
asset_audit: AssetAuditTable;
asset_exif: AssetExifTable; asset_exif: AssetExifTable;
asset_face: AssetFaceTable; asset_face: AssetFaceTable;
asset_face_audit: AssetFaceAuditTable; asset_face_audit: AssetFaceAuditTable;
@@ -173,7 +174,6 @@ export interface DB {
asset_metadata: AssetMetadataTable; asset_metadata: AssetMetadataTable;
asset_metadata_audit: AssetMetadataAuditTable; asset_metadata_audit: AssetMetadataAuditTable;
asset_job_status: AssetJobStatusTable; asset_job_status: AssetJobStatusTable;
asset_audit: AssetAuditTable;
audit: AuditTable; audit: AuditTable;

View File

@@ -1,11 +1,11 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { MemoryTable } from 'src/schema/tables/memory.table'; 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') @Table('memory_asset_audit')
export class MemoryAssetAuditTable { export class MemoryAssetAuditTable {
@PrimaryGeneratedUuidV7Column() @PrimaryGeneratedUuidV7Column()
id!: string; id!: Generated<string>;
@ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' })
memoryId!: string; memoryId!: string;
@@ -14,5 +14,5 @@ export class MemoryAssetAuditTable {
assetId!: string; assetId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) @CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
deletedAt!: Date; deletedAt!: Generated<Timestamp>;
} }

View File

@@ -42,6 +42,7 @@ describe(JobService.name, () => {
{ name: JobName.PersonCleanup }, { name: JobName.PersonCleanup },
{ name: JobName.MemoryCleanup }, { name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup }, { name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup }, { name: JobName.AuditLogCleanup },
{ name: JobName.MemoryGenerate }, { name: JobName.MemoryGenerate },
{ name: JobName.UserSyncUsage }, { name: JobName.UserSyncUsage },

View File

@@ -281,6 +281,7 @@ export class JobService extends BaseService {
{ name: JobName.PersonCleanup }, { name: JobName.PersonCleanup },
{ name: JobName.MemoryCleanup }, { name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup }, { name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup }, { name: JobName.AuditLogCleanup },
); );
} }

View File

@@ -1,8 +1,9 @@
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { Insertable } from 'kysely'; import { Insertable } from 'kysely';
import { DateTime } from 'luxon'; import { DateTime, Duration } from 'luxon';
import { Writable } from 'node:stream'; import { Writable } from 'node:stream';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { OnJob } from 'src/decorators';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@@ -15,7 +16,16 @@ import {
SyncItem, SyncItem,
SyncStreamDto, SyncStreamDto,
} from 'src/dtos/sync.dto'; } 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 { SyncQueryOptions } from 'src/repositories/sync.repository';
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table'; import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
@@ -32,6 +42,8 @@ type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
}; };
const COMPLETE_ID = 'complete'; const COMPLETE_ID = 'complete';
const MAX_DAYS = 30;
const MAX_DURATION = Duration.fromObject({ days: MAX_DAYS });
const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({ const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({
...data, ...data,
@@ -137,19 +149,24 @@ export class SyncService extends BaseService {
} }
const isPendingSyncReset = await this.sessionRepository.isPendingSyncReset(session.id); const isPendingSyncReset = await this.sessionRepository.isPendingSyncReset(session.id);
if (isPendingSyncReset) { if (isPendingSyncReset) {
send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} }); send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} });
response.end(); response.end();
return; 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 { nowId } = await this.syncCheckpointRepository.getNow();
const options: SyncQueryOptions = { nowId, userId: auth.user.id }; 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<SyncRequestType, () => Promise<void>> = { const handlers: Record<SyncRequestType, () => Promise<void>> = {
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap), [SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap),
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap), [SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
@@ -180,9 +197,41 @@ export class SyncService extends BaseService {
await handler(); await handler();
} }
send(response, { type: SyncEntityType.SyncCompleteV1, ids: [nowId], data: {} });
response.end(); 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) { private async syncAuthUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
const upsertType = SyncEntityType.AuthUserV1; const upsertType = SyncEntityType.AuthUserV1;
const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] }); 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) { private async syncPeopleV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
const deleteType = SyncEntityType.PersonDeleteV1; 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) { for await (const { id, ...data } of deletes) {
send(response, { type: deleteType, ids: [id], data }); send(response, { type: deleteType, ids: [id], data });
} }
const upsertType = SyncEntityType.PersonV1; 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) { for await (const { updateId, ...data } of upserts) {
send(response, { type: upsertType, ids: [updateId], data }); send(response, { type: upsertType, ids: [updateId], data });
} }

View File

@@ -275,6 +275,9 @@ export interface QueueStatus {
} }
export type JobItem = export type JobItem =
// Audit
| { name: JobName.AuditTableCleanup; data?: IBaseJob }
// Backups // Backups
| { name: JobName.DatabaseBackup; data?: IBaseJob } | { name: JobName.DatabaseBackup; data?: IBaseJob }

View File

@@ -35,7 +35,7 @@ export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif
primaryAssetId: assets[0].id, primaryAssetId: assets[0].id,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'),
updateId: 'uuid-v7', updateId: expect.any(String),
}; };
}; };

View File

@@ -1,5 +1,6 @@
import { Tag } from 'src/database'; import { Tag } from 'src/database';
import { TagResponseDto } from 'src/dtos/tag.dto'; import { TagResponseDto } from 'src/dtos/tag.dto';
import { newUuidV7 } from 'test/small.factory';
const parent = Object.freeze<Tag>({ const parent = Object.freeze<Tag>({
id: 'tag-parent', id: 'tag-parent',
@@ -37,7 +38,10 @@ const color = {
parentId: null, parentId: null,
}; };
const upsert = { userId: 'tag-user', updateId: 'uuid-v7' }; const upsert = {
userId: 'tag-user',
updateId: newUuidV7(),
};
export const tagStub = { export const tagStub = {
tag, tag,

View File

@@ -258,6 +258,12 @@ export class SyncTestContext extends MediumTestContext<SyncService> {
return stream.getResponse(); 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 }>) { async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
const acks: Record<string, string> = {}; const acks: Record<string, string> = {};
const syncAcks: string[] = []; const syncAcks: string[] = [];

View File

@@ -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<DB>;
const setup = (db?: Kysely<DB>) => {
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 <T extends keyof DB>(db: Kysely<DB>, 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);
});
});
});

View File

@@ -74,11 +74,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}, },
type: SyncEntityType.AlbumAssetExifCreateV1, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(response).toHaveLength(2);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.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.AssetExifsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(2); 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 () => { 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 { session } = await ctx.newSession({ userId: user3.id });
const authUser3 = factory.auth({ session, user: user3 }); const authUser3 = factory.auth({ session, user: user3 });
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(0); 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 () => { 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, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(response).toHaveLength(2);
// ack initial album asset exif sync // ack initial album asset exif sync
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -174,11 +184,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetExifCreateV1, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(newResponse).toHaveLength(5);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(firstAlbumResponse).toHaveLength(2);
await ctx.syncAckAll(auth, firstAlbumResponse); await ctx.syncAckAll(auth, firstAlbumResponse);
@@ -224,8 +234,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
type: SyncEntityType.AlbumAssetExifBackfillV1, type: SyncEntityType.AlbumAssetExifBackfillV1,
}, },
backfillSyncAck, backfillSyncAck,
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(response).toHaveLength(2);
// ack initial album asset sync // ack initial album asset sync
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -244,11 +254,11 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetExifCreateV1, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(newResponse).toHaveLength(2);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
expect(response).toHaveLength(2);
expect(response).toEqual([ expect(response).toEqual([
updateSyncAck, updateSyncAck,
{ {
@@ -272,6 +281,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetExifCreateV1, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -283,9 +293,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
city: 'New City', city: 'New City',
}); });
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([
expect(updateResponse).toHaveLength(1);
expect(updateResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@@ -294,6 +302,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetExifUpdateV1, type: SyncEntityType.AlbumAssetExifUpdateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
}); });
@@ -330,8 +339,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetExifCreateV1, type: SyncEntityType.AlbumAssetExifCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(response).toHaveLength(3);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -342,8 +351,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
city: 'Delayed Exif', city: 'Delayed Exif',
}); });
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([
expect(updateResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@@ -352,7 +360,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetExifUpdateV1, type: SyncEntityType.AlbumAssetExifUpdateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
expect(updateResponse).toHaveLength(1);
}); });
}); });

View File

@@ -58,7 +58,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(response).toHaveLength(2);
expect(response).toEqual([ expect(response).toEqual([
updateSyncAck, updateSyncAck,
{ {
@@ -83,10 +82,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}, },
type: SyncEntityType.AlbumAssetCreateV1, type: SyncEntityType.AlbumAssetCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.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.AssetsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(2); 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 () => { 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 { session } = await ctx.newSession({ userId: user3.id });
const authUser3 = factory.auth({ session, user: user3 }); const authUser3 = factory.auth({ session, user: user3 });
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(0); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(response).toHaveLength(2);
expect(response).toEqual([ expect(response).toEqual([
updateSyncAck, updateSyncAck,
{ {
@@ -143,6 +152,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetCreateV1, type: SyncEntityType.AlbumAssetCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
// ack initial album asset sync // ack initial album asset sync
@@ -176,10 +186,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetCreateV1, type: SyncEntityType.AlbumAssetCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(firstAlbumResponse).toHaveLength(2);
expect(firstAlbumResponse).toEqual([ expect(firstAlbumResponse).toEqual([
updateSyncAck, updateSyncAck,
{ {
@@ -206,6 +216,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetCreateV1, type: SyncEntityType.AlbumAssetCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, firstAlbumResponse); 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 }); await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
// expect(response).toHaveLength(2);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -223,6 +233,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
type: SyncEntityType.AlbumAssetBackfillV1, type: SyncEntityType.AlbumAssetBackfillV1,
}, },
backfillSyncAck, backfillSyncAck,
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
// ack initial album asset sync // ack initial album asset sync
@@ -242,10 +253,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetCreateV1, type: SyncEntityType.AlbumAssetCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(response).toHaveLength(2);
expect(response).toEqual([ expect(response).toEqual([
updateSyncAck, updateSyncAck,
{ {
@@ -268,6 +279,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetCreateV1, type: SyncEntityType.AlbumAssetCreateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -280,7 +292,6 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}); });
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(updateResponse).toHaveLength(1);
expect(updateResponse).toEqual([ expect(updateResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -290,6 +301,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
}), }),
type: SyncEntityType.AlbumAssetUpdateV1, type: SyncEntityType.AlbumAssetUpdateV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
}); });
}); });

View File

@@ -28,7 +28,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -38,10 +37,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -61,10 +60,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -86,10 +85,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { asset } = await ctx.newAsset({ ownerId: user2.id });
const { album } = await ctx.newAlbum({ ownerId: user2.id }); const { album } = await ctx.newAlbum({ ownerId: user2.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.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 () => { 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 }); await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -124,6 +123,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
// ack initial album to asset sync // ack initial album to asset sync
@@ -148,10 +148,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -172,6 +172,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -179,7 +180,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
await wait(2); await wait(2);
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -189,10 +189,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetDeleteV1, type: SyncEntityType.AlbumToAssetDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -213,6 +213,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -220,7 +221,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
await wait(2); await wait(2);
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -230,10 +230,11 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetDeleteV1, type: SyncEntityType.AlbumToAssetDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -254,11 +254,12 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await albumRepo.delete(album.id); await albumRepo.delete(album.id);
await wait(2); await wait(2);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]);
}); });
}); });

View File

@@ -34,6 +34,7 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, 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 { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -56,10 +56,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
await ctx.syncAckAll(auth, response); 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 }); await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.Viewer });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -86,10 +86,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
expect(response).toHaveLength(1);
await ctx.syncAckAll(auth, response); 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 }); await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
@@ -115,10 +115,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserDeleteV1, type: SyncEntityType.AlbumUserDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -145,10 +145,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); 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 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 }); await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.Viewer });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
@@ -178,10 +183,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); 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 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 }); await albumUserRepo.delete({ albumsId: album.id, usersId: user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
@@ -210,10 +220,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserDeleteV1, type: SyncEntityType.AlbumUserDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -243,6 +253,7 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
// ack initial user // ack initial user
@@ -285,10 +296,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumUsersV1]);
}); });
}); });
}); });

View File

@@ -24,7 +24,6 @@ describe(SyncRequestType.AlbumsV1, () => {
const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -35,10 +34,11 @@ describe(SyncRequestType.AlbumsV1, () => {
}), }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { album } = await ctx.newAlbum({ ownerId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -55,10 +54,11 @@ describe(SyncRequestType.AlbumsV1, () => {
}), }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { album } = await ctx.newAlbum({ ownerId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -76,12 +75,12 @@ describe(SyncRequestType.AlbumsV1, () => {
}), }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await albumRepo.delete(album.id); await albumRepo.delete(album.id);
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -90,10 +89,11 @@ describe(SyncRequestType.AlbumsV1, () => {
}, },
type: SyncEntityType.AlbumDeleteV1, type: SyncEntityType.AlbumDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]);
}); });
describe('shared albums', () => { describe('shared albums', () => {
@@ -104,17 +104,17 @@ describe(SyncRequestType.AlbumsV1, () => {
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: album.id }), data: expect.objectContaining({ id: album.id }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: album.id }), data: expect.objectContaining({ id: album.id }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }), data: expect.objectContaining({ id: userAlbum.id }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.Editor }); await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.Editor });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: user2Album.id }), data: expect.objectContaining({ id: user2Album.id }),
type: SyncEntityType.AlbumV1, type: SyncEntityType.AlbumV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); 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 ctx.syncAckAll(auth, response);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]);
await albumRepo.delete(album.id); await albumRepo.delete(album.id);
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { albumId: album.id }, data: { albumId: album.id },
type: SyncEntityType.AlbumDeleteV1, type: SyncEntityType.AlbumDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); 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 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 }); await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
@@ -218,10 +225,11 @@ describe(SyncRequestType.AlbumsV1, () => {
data: { albumId: album.id }, data: { albumId: album.id },
type: SyncEntityType.AlbumDeleteV1, type: SyncEntityType.AlbumDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumsV1]);
}); });
}); });
}); });

View File

@@ -24,7 +24,6 @@ describe(SyncRequestType.AssetExifsV1, () => {
await ctx.newExif({ assetId: asset.id, make: 'Canon' }); await ctx.newExif({ assetId: asset.id, make: 'Canon' });
const response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -57,10 +56,11 @@ describe(SyncRequestType.AssetExifsV1, () => {
}, },
type: SyncEntityType.AssetExifV1, type: SyncEntityType.AssetExifV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { session } = await ctx.newSession({ userId: user2.id });
const auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); expect.objectContaining({ type: SyncEntityType.AssetExifV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetExifsV1]);
}); });
}); });

View File

@@ -26,7 +26,6 @@ describe(SyncEntityType.AssetFaceV1, () => {
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -44,10 +43,11 @@ describe(SyncEntityType.AssetFaceV1, () => {
}), }),
type: 'AssetFaceV1', type: 'AssetFaceV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted asset face', async () => {
@@ -58,7 +58,6 @@ describe(SyncEntityType.AssetFaceV1, () => {
await personRepo.deleteAssetFace(assetFace.id); await personRepo.deleteAssetFace(assetFace.id);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -67,10 +66,11 @@ describe(SyncEntityType.AssetFaceV1, () => {
}, },
type: 'AssetFaceDeleteV1', type: 'AssetFaceDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { assetFace } = await ctx.newAssetFace({ assetId: asset.id });
const auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([
expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); expect.objectContaining({ type: SyncEntityType.AssetFaceV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]);
await personRepo.deleteAssetFace(assetFace.id); 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]);
}); });
}); });

View File

@@ -26,7 +26,6 @@ describe(SyncEntityType.AssetMetadataV1, () => {
await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -37,10 +36,11 @@ describe(SyncEntityType.AssetMetadataV1, () => {
}, },
type: 'AssetMetadataV1', type: 'AssetMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should update asset metadata', async () => {
@@ -51,7 +51,6 @@ describe(SyncEntityType.AssetMetadataV1, () => {
await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -62,6 +61,7 @@ describe(SyncEntityType.AssetMetadataV1, () => {
}, },
type: 'AssetMetadataV1', type: 'AssetMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -79,10 +79,11 @@ describe(SyncEntityType.AssetMetadataV1, () => {
}, },
type: 'AssetMetadataV1', type: 'AssetMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, updatedResponse); 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' } }]); await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -106,6 +106,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => {
}, },
type: 'AssetMetadataV1', type: 'AssetMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -121,6 +122,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => {
}, },
type: 'AssetMetadataDeleteV1', type: 'AssetMetadataDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
}); });
}); });

View File

@@ -40,7 +40,6 @@ describe(SyncEntityType.AssetV1, () => {
}); });
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -64,10 +63,11 @@ describe(SyncEntityType.AssetV1, () => {
}, },
type: 'AssetV1', type: 'AssetV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted asset', async () => {
@@ -77,7 +77,6 @@ describe(SyncEntityType.AssetV1, () => {
await assetRepo.remove(asset); await assetRepo.remove(asset);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -86,10 +85,11 @@ describe(SyncEntityType.AssetV1, () => {
}, },
type: 'AssetDeleteV1', type: 'AssetDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { asset } = await ctx.newAsset({ ownerId: user2.id });
const auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([
expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); expect.objectContaining({ type: SyncEntityType.AssetV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
await assetRepo.remove(asset); await assetRepo.remove(asset);
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([
expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
}); });
}); });

View File

@@ -22,7 +22,6 @@ describe(SyncEntityType.AuthUserV1, () => {
const { auth, user, ctx } = await setup(await getKyselyDB()); const { auth, user, ctx } = await setup(await getKyselyDB());
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -43,10 +42,11 @@ describe(SyncEntityType.AuthUserV1, () => {
}, },
type: 'AuthUserV1', type: 'AuthUserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 userRepo = ctx.get(UserRepository);
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -65,6 +64,7 @@ describe(SyncEntityType.AuthUserV1, () => {
}), }),
type: 'AuthUserV1', type: 'AuthUserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -72,7 +72,6 @@ describe(SyncEntityType.AuthUserV1, () => {
await userRepo.update(user.id, { isAdmin: true }); await userRepo.update(user.id, { isAdmin: true });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -82,6 +81,7 @@ describe(SyncEntityType.AuthUserV1, () => {
}), }),
type: 'AuthUserV1', type: 'AuthUserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
}); });
}); });

View File

@@ -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<DB>;
const setup = async (db?: Kysely<DB>) => {
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]);
});
});

View File

@@ -25,7 +25,6 @@ describe(SyncEntityType.MemoryToAssetV1, () => {
await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id }); await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id });
const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -35,10 +34,11 @@ describe(SyncEntityType.MemoryToAssetV1, () => {
}, },
type: 'MemoryToAssetV1', type: 'MemoryToAssetV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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]); await memoryRepo.removeAssetIds(memory.id, [asset.id]);
const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -60,10 +59,11 @@ describe(SyncEntityType.MemoryToAssetV1, () => {
}, },
type: 'MemoryToAssetDeleteV1', type: 'MemoryToAssetDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); const { memory } = await ctx.newMemory({ ownerId: user2.id });
await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.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])).toEqual([
expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1); expect.objectContaining({ type: SyncEntityType.MemoryToAssetV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoryToAssetsV1]);
await memoryRepo.removeAssetIds(memory.id, [asset.id]); 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]);
}); });
}); });

View File

@@ -23,7 +23,6 @@ describe(SyncEntityType.MemoryV1, () => {
const { memory } = await ctx.newMemory({ ownerId: user1.id }); const { memory } = await ctx.newMemory({ ownerId: user1.id });
const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -43,10 +42,11 @@ describe(SyncEntityType.MemoryV1, () => {
}, },
type: 'MemoryV1', type: 'MemoryV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted memory', async () => {
@@ -56,7 +56,6 @@ describe(SyncEntityType.MemoryV1, () => {
await memoryRepo.delete(memory.id); await memoryRepo.delete(memory.id);
const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -65,10 +64,11 @@ describe(SyncEntityType.MemoryV1, () => {
}, },
type: 'MemoryDeleteV1', type: 'MemoryDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { memory } = await ctx.newMemory({ ownerId: user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: memory.id }), data: expect.objectContaining({ id: memory.id }),
type: 'MemoryV1', type: 'MemoryV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await memoryRepo.update(memory.id, { seenAt: new Date() }); await memoryRepo.update(memory.id, { seenAt: new Date() });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: memory.id }), data: expect.objectContaining({ id: memory.id }),
type: 'MemoryV1', type: 'MemoryV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 { user: user2 } = await ctx.newUser();
const { memory } = await ctx.newMemory({ ownerId: user2.id }); 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 memoryRepo.delete(memory.id);
await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.MemoriesV1]);
}); });
}); });

View File

@@ -26,7 +26,6 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
await ctx.newExif({ assetId: asset.id, make: 'Canon' }); await ctx.newExif({ assetId: asset.id, make: 'Canon' });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -59,10 +58,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}, },
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
await ctx.newExif({ assetId: asset.id, make: 'Canon' }); await ctx.newExif({ assetId: asset.id, make: 'Canon' });
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); 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 () => { 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 { session } = await ctx.newSession({ userId: user3.id });
const authUser3 = factory.auth({ session, user: user3 }); const authUser3 = factory.auth({ session, user: user3 });
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{ {
@@ -112,6 +117,7 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, 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 }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(newResponse).toHaveLength(2);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -133,10 +138,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -160,15 +165,15 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
// This checks that our ack upsert is correct // 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 }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(newResponse).toHaveLength(2);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
@@ -182,10 +187,11 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -212,13 +217,13 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(newResponse).toHaveLength(3);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
@@ -239,9 +244,10 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetExifsV1]);
}); });
}); });

View File

@@ -46,7 +46,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -70,10 +69,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}, },
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted partner asset', async () => {
@@ -86,7 +86,6 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
await assetRepo.remove(asset); await assetRepo.remove(asset);
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -95,10 +94,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}, },
type: SyncEntityType.PartnerAssetDeleteV1, type: SyncEntityType.PartnerAssetDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
await ctx.newAsset({ ownerId: user2.id }); await ctx.newAsset({ ownerId: user2.id });
await userRepo.delete({ id: user2.id }, true); 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 () => { 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(); const { user: user2 } = await ctx.newUser();
await ctx.newAsset({ ownerId: user2.id }); await ctx.newAsset({ ownerId: user2.id });
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.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 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 () => { 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 }); const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
await ctx.newPartner({ sharedById: user2.id, sharedWithId: 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.AssetsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); expect.objectContaining({ type: SyncEntityType.AssetV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
await assetRepo.remove(asset); await assetRepo.remove(asset);
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); 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 () => { 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 { asset } = await ctx.newAsset({ ownerId: user2.id });
const auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); expect.objectContaining({ type: SyncEntityType.AssetV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
await assetRepo.remove(asset); await assetRepo.remove(asset);
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -179,13 +193,13 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(newResponse).toHaveLength(2);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -199,10 +213,11 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -227,12 +241,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(newResponse).toHaveLength(3);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -253,9 +267,10 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
}); });
}); });

View File

@@ -29,7 +29,6 @@ describe(SyncRequestType.PartnerStacksV1, () => {
const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]); const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]);
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -42,10 +41,11 @@ describe(SyncRequestType.PartnerStacksV1, () => {
}, },
type: SyncEntityType.PartnerStackV1, type: SyncEntityType.PartnerStackV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted partner stack', async () => {
@@ -58,7 +58,6 @@ describe(SyncRequestType.PartnerStacksV1, () => {
await stackRepo.delete(stack.id); await stackRepo.delete(stack.id);
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.stringContaining('PartnerStackDeleteV1'), ack: expect.stringContaining('PartnerStackDeleteV1'),
@@ -67,10 +66,11 @@ describe(SyncRequestType.PartnerStacksV1, () => {
}, },
type: SyncEntityType.PartnerStackDeleteV1, type: SyncEntityType.PartnerStackDeleteV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 }); const { asset } = await ctx.newAsset({ ownerId: user2.id });
await ctx.newStack({ ownerId: user2.id }, [asset.id]); await ctx.newStack({ ownerId: user2.id }, [asset.id]);
await userRepo.delete({ id: user2.id }, true); 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 () => { 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 }); const { asset } = await ctx.newAsset({ ownerId: user2.id });
await ctx.newStack({ ownerId: user2.id }, [asset.id]); await ctx.newStack({ ownerId: user2.id }, [asset.id]);
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user.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 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 () => { 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 { asset } = await ctx.newAsset({ ownerId: user.id });
const { stack } = await ctx.newStack({ ownerId: user.id }, [asset.id]); const { stack } = await ctx.newStack({ ownerId: user.id }, [asset.id]);
await ctx.newPartner({ sharedById: user2.id, sharedWithId: user.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.StacksV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); expect.objectContaining({ type: SyncEntityType.StackV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]);
await stackRepo.delete(stack.id); await stackRepo.delete(stack.id);
await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); 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 () => { 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 { stack } = await ctx.newStack({ ownerId: user2.id }, [asset.id]);
const auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); expect.objectContaining({ type: SyncEntityType.StackV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]);
await stackRepo.delete(stack.id); await stackRepo.delete(stack.id);
await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth2, [SyncRequestType.StacksV1])).resolves.toEqual([
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toHaveLength(0); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.stringContaining('PartnerStackV1'), ack: expect.stringContaining('PartnerStackV1'),
@@ -149,12 +163,12 @@ describe(SyncRequestType.PartnerStacksV1, () => {
}), }),
type: SyncEntityType.PartnerStackV1, type: SyncEntityType.PartnerStackV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await ctx.newPartner({ sharedById: user3.id, sharedWithId: user.id }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]);
expect(newResponse).toHaveLength(2);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.stringContaining(SyncEntityType.PartnerStackBackfillV1), ack: expect.stringContaining(SyncEntityType.PartnerStackBackfillV1),
@@ -168,10 +182,11 @@ describe(SyncRequestType.PartnerStacksV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.stringContaining(SyncEntityType.PartnerStackV1), ack: expect.stringContaining(SyncEntityType.PartnerStackV1),
@@ -198,12 +212,12 @@ describe(SyncRequestType.PartnerStacksV1, () => {
}), }),
type: SyncEntityType.PartnerStackV1, type: SyncEntityType.PartnerStackV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1]);
expect(newResponse).toHaveLength(3);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -224,9 +238,10 @@ describe(SyncRequestType.PartnerStacksV1, () => {
}), }),
type: SyncEntityType.PartnerStackV1, type: SyncEntityType.PartnerStackV1,
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerStacksV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerStacksV1]);
}); });
}); });

View File

@@ -26,7 +26,6 @@ describe(SyncEntityType.PartnerV1, () => {
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -37,10 +36,11 @@ describe(SyncEntityType.PartnerV1, () => {
}, },
type: 'PartnerV1', type: 'PartnerV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted partner', async () => {
@@ -53,22 +53,20 @@ describe(SyncEntityType.PartnerV1, () => {
await partnerRepo.remove(partner); await partnerRepo.remove(partner);
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(response).toHaveLength(1); expect(response).toEqual([
expect(response).toEqual( {
expect.arrayContaining([ ack: expect.any(String),
{ data: {
ack: expect.any(String), sharedById: partner.sharedById,
data: { sharedWithId: partner.sharedWithId,
sharedById: partner.sharedById,
sharedWithId: partner.sharedWithId,
},
type: 'PartnerDeleteV1',
}, },
]), type: 'PartnerDeleteV1',
); },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { partner: partner2 } = await ctx.newPartner({ sharedById: user1.id, sharedWithId: user2.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(response).toHaveLength(2); expect(response).toEqual([
expect(response).toEqual( {
expect.arrayContaining([ ack: expect.any(String),
{ data: {
ack: expect.any(String), inTimeline: partner1.inTimeline,
data: { sharedById: partner1.sharedById,
inTimeline: partner1.inTimeline, sharedWithId: partner1.sharedWithId,
sharedById: partner1.sharedById,
sharedWithId: partner1.sharedWithId,
},
type: 'PartnerV1',
}, },
{ type: 'PartnerV1',
ack: expect.any(String), },
data: { {
inTimeline: partner2.inTimeline, ack: expect.any(String),
sharedById: partner2.sharedById, data: {
sharedWithId: partner2.sharedWithId, inTimeline: partner2.inTimeline,
}, sharedById: partner2.sharedById,
type: 'PartnerV1', sharedWithId: partner2.sharedWithId,
}, },
]), type: 'PartnerV1',
); },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -127,6 +122,7 @@ describe(SyncEntityType.PartnerV1, () => {
}, },
type: 'PartnerV1', type: 'PartnerV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -137,7 +133,6 @@ describe(SyncEntityType.PartnerV1, () => {
); );
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -148,10 +143,11 @@ describe(SyncEntityType.PartnerV1, () => {
}, },
type: 'PartnerV1', type: 'PartnerV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 { user: user3 } = await ctx.newUser();
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user3.id }); 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 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 () => { 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 ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
await userRepo.delete({ id: user2.id }, true); await userRepo.delete({ id: user2.id }, true);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnersV1]);
}); });
}); });

View File

@@ -24,7 +24,6 @@ describe(SyncEntityType.PersonV1, () => {
const { person } = await ctx.newPerson({ ownerId: auth.user.id }); const { person } = await ctx.newPerson({ ownerId: auth.user.id });
const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -40,10 +39,11 @@ describe(SyncEntityType.PersonV1, () => {
}), }),
type: 'PersonV1', type: 'PersonV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted person', async () => {
@@ -53,7 +53,6 @@ describe(SyncEntityType.PersonV1, () => {
await personRepo.delete([person.id]); await personRepo.delete([person.id]);
const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -62,10 +61,11 @@ describe(SyncEntityType.PersonV1, () => {
}, },
type: 'PersonDeleteV1', type: 'PersonDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { person } = await ctx.newPerson({ ownerId: user2.id });
const auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toEqual([
expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); expect.objectContaining({ type: SyncEntityType.PersonV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]);
await personRepo.delete([person.id]); 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]);
}); });
}); });

View File

@@ -21,8 +21,7 @@ describe(SyncEntityType.SyncResetV1, () => {
it('should work', async () => { it('should work', async () => {
const { auth, ctx } = await setup(); const { auth, ctx } = await setup();
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
expect(response).toEqual([]);
}); });
it('should detect a pending sync reset', async () => { it('should detect a pending sync reset', async () => {
@@ -41,7 +40,10 @@ describe(SyncEntityType.SyncResetV1, () => {
await ctx.newAsset({ ownerId: user.id }); 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, { await ctx.get(SessionRepository).update(auth.session!.id, {
isPendingSyncReset: true, isPendingSyncReset: true,
@@ -62,9 +64,8 @@ describe(SyncEntityType.SyncResetV1, () => {
}); });
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([ await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([
expect.objectContaining({ expect.objectContaining({ type: SyncEntityType.AssetV1 }),
type: SyncEntityType.AssetV1, expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
}),
]); ]);
}); });
@@ -86,9 +87,8 @@ describe(SyncEntityType.SyncResetV1, () => {
const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
expect(postResetResponse).toEqual([ expect(postResetResponse).toEqual([
expect.objectContaining({ expect.objectContaining({ type: SyncEntityType.AssetV1 }),
type: SyncEntityType.AssetV1, expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
}),
]); ]);
}); });
}); });

View File

@@ -25,7 +25,6 @@ describe(SyncEntityType.StackV1, () => {
const { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); const { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]);
const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.stringContaining('StackV1'), ack: expect.stringContaining('StackV1'),
@@ -38,10 +37,11 @@ describe(SyncEntityType.StackV1, () => {
}, },
type: 'StackV1', type: 'StackV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted stack', async () => {
@@ -53,17 +53,17 @@ describe(SyncEntityType.StackV1, () => {
await stackRepo.delete(stack.id); await stackRepo.delete(stack.id);
const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.stringContaining('StackDeleteV1'), ack: expect.stringContaining('StackDeleteV1'),
data: { stackId: stack.id }, data: { stackId: stack.id },
type: 'StackDeleteV1', type: 'StackDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 { stack } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]);
const response = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); 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 ctx.syncAckAll(auth, response);
await stackRepo.update(stack.id, { primaryAssetId: asset2.id }); await stackRepo.update(stack.id, { primaryAssetId: asset2.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.StacksV1]); 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([ expect(newResponse).toEqual([
{ {
ack: expect.stringContaining('StackV1'), ack: expect.stringContaining('StackV1'),
data: expect.objectContaining({ id: stack.id, primaryAssetId: asset2.id }), data: expect.objectContaining({ id: stack.id, primaryAssetId: asset2.id }),
type: 'StackV1', type: 'StackV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, newResponse); 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 () => { 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 { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id });
const { stack } = await ctx.newStack({ ownerId: user2.id }, [asset1.id, asset2.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 stackRepo.delete(stack.id);
await expect(ctx.syncStream(auth, [SyncRequestType.StacksV1])).resolves.toEqual([]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.StacksV1]);
}); });
}); });

View File

@@ -25,7 +25,6 @@ describe(SyncEntityType.UserMetadataV1, () => {
await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } });
const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -36,10 +35,11 @@ describe(SyncEntityType.UserMetadataV1, () => {
}, },
type: 'UserMetadataV1', type: 'UserMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { it('should update user metadata', async () => {
@@ -49,7 +49,6 @@ describe(SyncEntityType.UserMetadataV1, () => {
await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } });
const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -60,6 +59,7 @@ describe(SyncEntityType.UserMetadataV1, () => {
}, },
type: 'UserMetadataV1', type: 'UserMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -77,10 +77,11 @@ describe(SyncEntityType.UserMetadataV1, () => {
}, },
type: 'UserMetadataV1', type: 'UserMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, updatedResponse); 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 } }); await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } });
const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -103,6 +103,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => {
}, },
type: 'UserMetadataV1', type: 'UserMetadataV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -118,6 +119,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => {
}, },
type: 'UserMetadataDeleteV1', type: 'UserMetadataDeleteV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
}); });
}); });

View File

@@ -28,7 +28,6 @@ describe(SyncEntityType.UserV1, () => {
} }
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -43,10 +42,11 @@ describe(SyncEntityType.UserV1, () => {
}, },
type: 'UserV1', type: 'UserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(2);
expect(response).toEqual( expect(response).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{ {
@@ -69,11 +68,12 @@ describe(SyncEntityType.UserV1, () => {
data: expect.objectContaining({ id: deleted.id }), data: expect.objectContaining({ id: deleted.id }),
type: 'UserV1', type: 'UserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]), ]),
); );
await ctx.syncAckAll(auth, response); 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 () => { it('should detect and sync a deleted user', async () => {
@@ -85,7 +85,6 @@ describe(SyncEntityType.UserV1, () => {
await userRepo.delete({ id: user.id }, true); await userRepo.delete({ id: user.id }, true);
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(2);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
@@ -99,10 +98,11 @@ describe(SyncEntityType.UserV1, () => {
data: expect.objectContaining({ id: authUser.id }), data: expect.objectContaining({ id: authUser.id }),
type: 'UserV1', type: 'UserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); 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 () => { 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 userRepo = ctx.get(UserRepository);
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: user.id }), data: expect.objectContaining({ id: user.id }),
type: 'UserV1', type: 'UserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
await ctx.syncAckAll(auth, response); await ctx.syncAckAll(auth, response);
@@ -125,13 +125,13 @@ describe(SyncEntityType.UserV1, () => {
const updated = await userRepo.update(auth.user.id, { name: 'new name' }); const updated = await userRepo.update(auth.user.id, { name: 'new name' });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: user.id, name: updated.name }), data: expect.objectContaining({ id: user.id, name: updated.name }),
type: 'UserV1', type: 'UserV1',
}, },
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]); ]);
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'node:crypto';
import { import {
Activity, Activity,
ApiKey, ApiKey,
@@ -17,14 +16,15 @@ import { MapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum';
import { OnThisDayData, UserMetadataItem } from 'src/types'; import { OnThisDayData, UserMetadataItem } from 'src/types';
import { v4, v7 } from 'uuid';
export const newUuid = () => randomUUID() as string; export const newUuid = () => v4();
export const newUuids = () => export const newUuids = () =>
Array.from({ length: 100 }) Array.from({ length: 100 })
.fill(0) .fill(0)
.map(() => newUuid()); .map(() => newUuid());
export const newDate = () => new Date(); 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 newSha1 = () => Buffer.from('this is a fake hash');
export const newEmbedding = () => { export const newEmbedding = () => {
const embedding = Array.from({ length: 512 }) const embedding = Array.from({ length: 512 })