mirror of https://github.com/immich-app/immich.git
feat: sync albums and album users (#18377)
parent
58af574241
commit
cd288533a1
@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncAlbumDeleteV1 {
|
||||||
|
/// Returns a new [SyncAlbumDeleteV1] instance.
|
||||||
|
SyncAlbumDeleteV1({
|
||||||
|
required this.albumId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumDeleteV1 &&
|
||||||
|
other.albumId == albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumDeleteV1[albumId=$albumId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumDeleteV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncAlbumUserDeleteV1 {
|
||||||
|
/// Returns a new [SyncAlbumUserDeleteV1] instance.
|
||||||
|
SyncAlbumUserDeleteV1({
|
||||||
|
required this.albumId,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserDeleteV1 &&
|
||||||
|
other.albumId == albumId &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumUserDeleteV1[albumId=$albumId, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumUserDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumUserDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumUserDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumUserDeleteV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumUserDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumUserDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumUserDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumUserDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumUserDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumUserDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumUserDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumUserDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumUserDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumUserDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,189 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncAlbumUserV1 {
|
||||||
|
/// Returns a new [SyncAlbumUserV1] instance.
|
||||||
|
SyncAlbumUserV1({
|
||||||
|
required this.albumId,
|
||||||
|
required this.role,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
SyncAlbumUserV1RoleEnum role;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 &&
|
||||||
|
other.albumId == albumId &&
|
||||||
|
other.role == role &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode) +
|
||||||
|
(role.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumUserV1[albumId=$albumId, role=$role, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
json[r'role'] = this.role;
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumUserV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumUserV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumUserV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumUserV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
role: SyncAlbumUserV1RoleEnum.fromJson(json[r'role'])!,
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumUserV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumUserV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumUserV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumUserV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumUserV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumUserV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumUserV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumUserV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumUserV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumUserV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
'role',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SyncAlbumUserV1RoleEnum {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const SyncAlbumUserV1RoleEnum._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const editor = SyncAlbumUserV1RoleEnum._(r'editor');
|
||||||
|
static const viewer = SyncAlbumUserV1RoleEnum._(r'viewer');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][SyncAlbumUserV1RoleEnum].
|
||||||
|
static const values = <SyncAlbumUserV1RoleEnum>[
|
||||||
|
editor,
|
||||||
|
viewer,
|
||||||
|
];
|
||||||
|
|
||||||
|
static SyncAlbumUserV1RoleEnum? fromJson(dynamic value) => SyncAlbumUserV1RoleEnumTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<SyncAlbumUserV1RoleEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumUserV1RoleEnum>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumUserV1RoleEnum.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [SyncAlbumUserV1RoleEnum] to String,
|
||||||
|
/// and [decode] dynamic data back to [SyncAlbumUserV1RoleEnum].
|
||||||
|
class SyncAlbumUserV1RoleEnumTypeTransformer {
|
||||||
|
factory SyncAlbumUserV1RoleEnumTypeTransformer() => _instance ??= const SyncAlbumUserV1RoleEnumTypeTransformer._();
|
||||||
|
|
||||||
|
const SyncAlbumUserV1RoleEnumTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(SyncAlbumUserV1RoleEnum data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a SyncAlbumUserV1RoleEnum.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
SyncAlbumUserV1RoleEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'editor': return SyncAlbumUserV1RoleEnum.editor;
|
||||||
|
case r'viewer': return SyncAlbumUserV1RoleEnum.viewer;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [SyncAlbumUserV1RoleEnumTypeTransformer] instance.
|
||||||
|
static SyncAlbumUserV1RoleEnumTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncAlbumV1 {
|
||||||
|
/// Returns a new [SyncAlbumV1] instance.
|
||||||
|
SyncAlbumV1({
|
||||||
|
required this.createdAt,
|
||||||
|
required this.description,
|
||||||
|
required this.id,
|
||||||
|
required this.isActivityEnabled,
|
||||||
|
required this.name,
|
||||||
|
required this.order,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.thumbnailAssetId,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
String description;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
bool isActivityEnabled;
|
||||||
|
|
||||||
|
String name;
|
||||||
|
|
||||||
|
AssetOrder order;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
String? thumbnailAssetId;
|
||||||
|
|
||||||
|
DateTime updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumV1 &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.description == description &&
|
||||||
|
other.id == id &&
|
||||||
|
other.isActivityEnabled == isActivityEnabled &&
|
||||||
|
other.name == name &&
|
||||||
|
other.order == order &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.thumbnailAssetId == thumbnailAssetId &&
|
||||||
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(description.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isActivityEnabled.hashCode) +
|
||||||
|
(name.hashCode) +
|
||||||
|
(order.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(thumbnailAssetId == null ? 0 : thumbnailAssetId!.hashCode) +
|
||||||
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumV1[createdAt=$createdAt, description=$description, id=$id, isActivityEnabled=$isActivityEnabled, name=$name, order=$order, ownerId=$ownerId, thumbnailAssetId=$thumbnailAssetId, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||||
|
json[r'name'] = this.name;
|
||||||
|
json[r'order'] = this.order;
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
if (this.thumbnailAssetId != null) {
|
||||||
|
json[r'thumbnailAssetId'] = this.thumbnailAssetId;
|
||||||
|
} else {
|
||||||
|
// json[r'thumbnailAssetId'] = null;
|
||||||
|
}
|
||||||
|
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumV1(
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
description: mapValueOfType<String>(json, r'description')!,
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||||
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
|
order: AssetOrder.fromJson(json[r'order'])!,
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
thumbnailAssetId: mapValueOfType<String>(json, r'thumbnailAssetId'),
|
||||||
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'createdAt',
|
||||||
|
'description',
|
||||||
|
'id',
|
||||||
|
'isActivityEnabled',
|
||||||
|
'name',
|
||||||
|
'order',
|
||||||
|
'ownerId',
|
||||||
|
'thumbnailAssetId',
|
||||||
|
'updatedAt',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION album_user_after_insert()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||||
|
WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows);
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION albums_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO albums_audit ("albumId", "userId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION album_users_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO albums_audit ("albumId", "userId")
|
||||||
|
SELECT "albumsId", "usersId"
|
||||||
|
FROM OLD;
|
||||||
|
|
||||||
|
IF pg_trigger_depth() = 1 THEN
|
||||||
|
INSERT INTO album_users_audit ("albumId", "userId")
|
||||||
|
SELECT "albumsId", "usersId"
|
||||||
|
FROM OLD;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE TABLE "albums_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "album_users_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_audit" ADD CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_users_audit" ADD CONSTRAINT "PK_f479a2e575b7ebc9698362c1688" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_update_id" ON "albums_shared_users_users" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_audit_album_id" ON "albums_audit" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_audit_user_id" ON "albums_audit" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_audit_deleted_at" ON "albums_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_audit_album_id" ON "album_users_audit" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_audit_user_id" ON "album_users_audit" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_audit_deleted_at" ON "album_users_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "albums_delete_audit"
|
||||||
|
AFTER DELETE ON "albums"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION albums_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_users_delete_audit"
|
||||||
|
AFTER DELETE ON "albums_shared_users_users"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() <= 1)
|
||||||
|
EXECUTE FUNCTION album_users_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_user_after_insert"
|
||||||
|
AFTER INSERT ON "albums_shared_users_users"
|
||||||
|
REFERENCING NEW TABLE AS "inserted_rows"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION album_user_after_insert();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_users_updated_at"
|
||||||
|
BEFORE UPDATE ON "albums_shared_users_users"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "albums_delete_audit" ON "albums";`.execute(db);
|
||||||
|
await sql`DROP TRIGGER "album_users_delete_audit" ON "albums_shared_users_users";`.execute(db);
|
||||||
|
await sql`DROP TRIGGER "album_user_after_insert" ON "albums_shared_users_users";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_albums_audit_album_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_albums_audit_user_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_albums_audit_deleted_at";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_audit_album_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_audit_user_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_audit_deleted_at";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_audit" DROP CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_users_audit" DROP CONSTRAINT "PK_f479a2e575b7ebc9698362c1688";`.execute(db);
|
||||||
|
await sql`DROP TABLE "albums_audit";`.execute(db);
|
||||||
|
await sql`DROP TABLE "album_users_audit";`.execute(db);
|
||||||
|
await sql`DROP FUNCTION album_user_after_insert;`.execute(db);
|
||||||
|
await sql`DROP FUNCTION albums_delete_audit;`.execute(db);
|
||||||
|
await sql`DROP FUNCTION album_users_delete_audit;`.execute(db);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('albums_audit')
|
||||||
|
export class AlbumAuditTable {
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_album_id' })
|
||||||
|
albumId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_user_id' })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_albums_audit_deleted_at' })
|
||||||
|
deletedAt!: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('album_users_audit')
|
||||||
|
export class AlbumUserAuditTable {
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_album_id' })
|
||||||
|
albumId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_user_id' })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_users_audit_deleted_at' })
|
||||||
|
deletedAt!: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||||
|
|
||||||
|
export const AfterInsertTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||||
|
TriggerFunction({
|
||||||
|
timing: 'after',
|
||||||
|
actions: ['insert'],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
@ -1,910 +0,0 @@
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
|
||||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
|
||||||
import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service';
|
|
||||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
|
||||||
import { factory } from 'test/small.factory';
|
|
||||||
import { getKyselyDB } from 'test/utils';
|
|
||||||
|
|
||||||
const setup = async () => {
|
|
||||||
const db = await getKyselyDB();
|
|
||||||
|
|
||||||
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
|
|
||||||
database: db,
|
|
||||||
repos: {
|
|
||||||
sync: 'real',
|
|
||||||
session: 'real',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = mediumFactory.userInsert();
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user.id });
|
|
||||||
const auth = factory.auth({
|
|
||||||
session,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await getRepository('user').create(user);
|
|
||||||
await getRepository('session').create(session);
|
|
||||||
|
|
||||||
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
|
||||||
const stream = mediumFactory.syncStream();
|
|
||||||
// Wait for 1ms to ensure all updates are available
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
||||||
await sut.stream(auth, stream, { types });
|
|
||||||
|
|
||||||
return stream.getResponse();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
sut,
|
|
||||||
auth,
|
|
||||||
mocks,
|
|
||||||
repos,
|
|
||||||
getRepository,
|
|
||||||
testSync,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe(SyncService.name, () => {
|
|
||||||
it('should have all the types in the ordering variable', () => {
|
|
||||||
for (const key in SyncRequestType) {
|
|
||||||
expect(SYNC_TYPES_ORDER).includes(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncEntityType.UserV1, () => {
|
|
||||||
it('should detect and sync the first user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
|
||||||
if (!user) {
|
|
||||||
expect.fail('First user should exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: user.deletedAt,
|
|
||||||
email: user.email,
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a soft deleted user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const deletedAt = new Date().toISOString();
|
|
||||||
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
|
||||||
const deleted = await getRepository('user').create(deletedUser);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(2);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: auth.user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt,
|
|
||||||
email: deleted.email,
|
|
||||||
id: deleted.id,
|
|
||||||
name: deleted.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [response[1].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user);
|
|
||||||
await userRepo.delete({ id: user.id }, true);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(2);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
type: 'UserDeleteV1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: auth.user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sync a user and then an update to that same user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: auth.user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
|
||||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(updatedSyncResponse).toHaveLength(1);
|
|
||||||
expect(updatedSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: updated.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
|
||||||
it('should detect and sync the first partner', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const user1 = auth.user;
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner.inTimeline,
|
|
||||||
sharedById: partner.sharedById,
|
|
||||||
sharedWithId: partner.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted partner', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user1 = auth.user;
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
await partnerRepo.remove(partner);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
sharedById: partner.sharedById,
|
|
||||||
sharedWithId: partner.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerDeleteV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a partner share both to and from another user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user1 = auth.user;
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(2);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner1.inTimeline,
|
|
||||||
sharedById: partner1.sharedById,
|
|
||||||
sharedWithId: partner1.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner2.inTimeline,
|
|
||||||
sharedById: partner2.sharedById,
|
|
||||||
sharedWithId: partner2.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sut.setAcks(auth, { acks: [response[1].ack] });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sync a partner and then an update to that same partner', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user1 = auth.user;
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner.inTimeline,
|
|
||||||
sharedById: partner.sharedById,
|
|
||||||
sharedWithId: partner.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const updated = await partnerRepo.update(
|
|
||||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
|
||||||
{ inTimeline: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(updatedSyncResponse).toHaveLength(1);
|
|
||||||
expect(updatedSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: updated.inTimeline,
|
|
||||||
sharedById: updated.sharedById,
|
|
||||||
sharedWithId: updated.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
const user3 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
|
||||||
|
|
||||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
|
||||||
|
|
||||||
await partnerRepo.remove(partner);
|
|
||||||
|
|
||||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a partner delete after a user is deleted', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
await userRepo.delete({ id: user2.id }, true);
|
|
||||||
|
|
||||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncEntityType.AssetV1, () => {
|
|
||||||
it('should detect and sync the first asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const date = new Date().toISOString();
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({
|
|
||||||
ownerId: auth.user.id,
|
|
||||||
checksum: Buffer.from(checksum, 'base64'),
|
|
||||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
|
||||||
fileCreatedAt: date,
|
|
||||||
fileModifiedAt: date,
|
|
||||||
localDateTime: date,
|
|
||||||
deletedAt: null,
|
|
||||||
});
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
ownerId: asset.ownerId,
|
|
||||||
thumbhash,
|
|
||||||
checksum,
|
|
||||||
deletedAt: asset.deletedAt,
|
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
|
||||||
fileModifiedAt: asset.fileModifiedAt,
|
|
||||||
isFavorite: asset.isFavorite,
|
|
||||||
localDateTime: asset.localDateTime,
|
|
||||||
type: asset.type,
|
|
||||||
visibility: asset.visibility,
|
|
||||||
},
|
|
||||||
type: 'AssetV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
},
|
|
||||||
type: 'AssetDeleteV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const auth2 = factory.auth({ session, user: user2 });
|
|
||||||
|
|
||||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
|
||||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
|
||||||
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
|
||||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
|
||||||
it('should detect and sync the first partner asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const date = new Date().toISOString();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({
|
|
||||||
ownerId: user2.id,
|
|
||||||
checksum: Buffer.from(checksum, 'base64'),
|
|
||||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
|
||||||
fileCreatedAt: date,
|
|
||||||
fileModifiedAt: date,
|
|
||||||
localDateTime: date,
|
|
||||||
deletedAt: null,
|
|
||||||
});
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
ownerId: asset.ownerId,
|
|
||||||
thumbhash,
|
|
||||||
checksum,
|
|
||||||
deletedAt: null,
|
|
||||||
fileCreatedAt: date,
|
|
||||||
fileModifiedAt: date,
|
|
||||||
isFavorite: false,
|
|
||||||
localDateTime: date,
|
|
||||||
type: asset.type,
|
|
||||||
visibility: asset.visibility,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.PartnerAssetV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted partner asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.PartnerAssetDeleteV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
|
||||||
|
|
||||||
await userRepo.delete({ id: user2.id }, true);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
expect(response).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
|
||||||
await partnerRepo.create(partner);
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
|
||||||
|
|
||||||
await partnerRepo.remove(partner);
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync an asset or asset delete for own user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const auth2 = factory.auth({ session, user: user2 });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
|
||||||
it('should detect and sync the first asset exif', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
city: null,
|
|
||||||
country: null,
|
|
||||||
dateTimeOriginal: null,
|
|
||||||
description: '',
|
|
||||||
exifImageHeight: null,
|
|
||||||
exifImageWidth: null,
|
|
||||||
exposureTime: null,
|
|
||||||
fNumber: null,
|
|
||||||
fileSizeInByte: null,
|
|
||||||
focalLength: null,
|
|
||||||
fps: null,
|
|
||||||
iso: null,
|
|
||||||
latitude: null,
|
|
||||||
lensModel: null,
|
|
||||||
longitude: null,
|
|
||||||
make: 'Canon',
|
|
||||||
model: null,
|
|
||||||
modifyDate: null,
|
|
||||||
orientation: null,
|
|
||||||
profileDescription: null,
|
|
||||||
projectionType: null,
|
|
||||||
rating: null,
|
|
||||||
state: null,
|
|
||||||
timeZone: null,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.AssetExifV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only sync asset exif for own user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const auth2 = factory.auth({ session, user: user2 });
|
|
||||||
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
|
||||||
it('should detect and sync the first partner asset exif', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
city: null,
|
|
||||||
country: null,
|
|
||||||
dateTimeOriginal: null,
|
|
||||||
description: '',
|
|
||||||
exifImageHeight: null,
|
|
||||||
exifImageWidth: null,
|
|
||||||
exposureTime: null,
|
|
||||||
fNumber: null,
|
|
||||||
fileSizeInByte: null,
|
|
||||||
focalLength: null,
|
|
||||||
fps: null,
|
|
||||||
iso: null,
|
|
||||||
latitude: null,
|
|
||||||
lensModel: null,
|
|
||||||
longitude: null,
|
|
||||||
make: 'Canon',
|
|
||||||
model: null,
|
|
||||||
modifyDate: null,
|
|
||||||
orientation: null,
|
|
||||||
profileDescription: null,
|
|
||||||
projectionType: null,
|
|
||||||
rating: null,
|
|
||||||
state: null,
|
|
||||||
timeZone: null,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.PartnerAssetExifV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync partner asset exif for own user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync partner asset exif for unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
const user3 = mediumFactory.userInsert();
|
|
||||||
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const authUser3 = factory.auth({ session, user: user3 });
|
|
||||||
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -0,0 +1,269 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(SyncRequestType.AlbumUsersV1, () => {
|
||||||
|
it('should sync an album user with the correct properties', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: albumUser.role,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
describe('owner', () => {
|
||||||
|
it('should detect and sync a new shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: albumUser.role,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an updated shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: AlbumUserRole.VIEWER,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shared user', () => {
|
||||||
|
it('should detect and sync a new shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: albumUser.role,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an updated shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const owner = mediumFactory.userInsert();
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||||
|
await albumRepo.create(
|
||||||
|
album,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(initialSyncResponse).toHaveLength(2);
|
||||||
|
const acks = [initialSyncResponse[1].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album.id,
|
||||||
|
role: AlbumUserRole.VIEWER,
|
||||||
|
userId: user.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const owner = mediumFactory.userInsert();
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||||
|
await albumRepo.create(
|
||||||
|
album,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(initialSyncResponse).toHaveLength(2);
|
||||||
|
const acks = [initialSyncResponse[1].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.delete({ albumsId: album.id, usersId: user.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album.id,
|
||||||
|
userId: user.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,220 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(SyncRequestType.AlbumsV1, () => {
|
||||||
|
it('should sync an album with the correct properties', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album.id,
|
||||||
|
name: album.albumName,
|
||||||
|
ownerId: album.ownerId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a new album', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album delete', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await albumRepo.delete(album.id);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shared albums', () => {
|
||||||
|
it('should detect and sync an album create', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: album.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album share (share before sync)', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await albumUserRepo.create({ usersId: auth.user.id, albumsId: album.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: album.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album share (share after sync)', async () => {
|
||||||
|
const { auth, getRepository, sut, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const userAlbum = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
const user2Album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await Promise.all([albumRepo.create(user2Album, [], []), albumRepo.create(userAlbum, [], [])]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: userAlbum.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await albumUserRepo.create({ usersId: auth.user.id, albumsId: user2Album.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: user2Album.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album delete`', async () => {
|
||||||
|
const { auth, getRepository, testSync, sut } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumRepo.delete(album.id);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: { albumId: album.id },
|
||||||
|
type: SyncEntityType.AlbumDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album unshare as an album delete', async () => {
|
||||||
|
const { auth, getRepository, testSync, sut } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: { albumId: album.id },
|
||||||
|
type: SyncEntityType.AlbumDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first asset exif', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: null,
|
||||||
|
exifImageWidth: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: 'Canon',
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
orientation: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only sync asset exif for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ session, user: user2 });
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||||
|
it('should detect and sync the first asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({
|
||||||
|
ownerId: auth.user.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
localDateTime: date,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
thumbhash,
|
||||||
|
checksum,
|
||||||
|
deletedAt: asset.deletedAt,
|
||||||
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
|
isFavorite: asset.isFavorite,
|
||||||
|
localDateTime: asset.localDateTime,
|
||||||
|
type: asset.type,
|
||||||
|
visibility: asset.visibility,
|
||||||
|
},
|
||||||
|
type: 'AssetV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: 'AssetDeleteV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ session, user: user2 });
|
||||||
|
|
||||||
|
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||||
|
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||||
|
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||||
|
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first partner asset exif', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: null,
|
||||||
|
exifImageWidth: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: 'Canon',
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
orientation: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync partner asset exif for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync partner asset exif for unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
const user3 = mediumFactory.userInsert();
|
||||||
|
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const authUser3 = factory.auth({ session, user: user3 });
|
||||||
|
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,208 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
||||||
|
it('should detect and sync the first partner asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({
|
||||||
|
ownerId: user2.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
localDateTime: date,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
thumbhash,
|
||||||
|
checksum,
|
||||||
|
deletedAt: null,
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
isFavorite: false,
|
||||||
|
localDateTime: date,
|
||||||
|
type: asset.type,
|
||||||
|
visibility: asset.visibility,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted partner asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||||
|
|
||||||
|
await userRepo.delete({ id: user2.id }, true);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
expect(response).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||||
|
await partnerRepo.create(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||||
|
|
||||||
|
await partnerRepo.remove(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ session, user: user2 });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||||
|
it('should detect and sync the first partner', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const user1 = auth.user;
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
sharedById: partner.sharedById,
|
||||||
|
sharedWithId: partner.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted partner', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user1 = auth.user;
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
await partnerRepo.remove(partner);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
sharedById: partner.sharedById,
|
||||||
|
sharedWithId: partner.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerDeleteV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a partner share both to and from another user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user1 = auth.user;
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner1.inTimeline,
|
||||||
|
sharedById: partner1.sharedById,
|
||||||
|
sharedWithId: partner1.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner2.inTimeline,
|
||||||
|
sharedById: partner2.sharedById,
|
||||||
|
sharedWithId: partner2.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [response[1].ack] });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync a partner and then an update to that same partner', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user1 = auth.user;
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
sharedById: partner.sharedById,
|
||||||
|
sharedWithId: partner.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const updated = await partnerRepo.update(
|
||||||
|
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||||
|
{ inTimeline: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(updatedSyncResponse).toHaveLength(1);
|
||||||
|
expect(updatedSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: updated.inTimeline,
|
||||||
|
sharedById: updated.sharedById,
|
||||||
|
sharedWithId: updated.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
const user3 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
|
||||||
|
await partnerRepo.remove(partner);
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a partner delete after a user is deleted', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await userRepo.delete({ id: user2.id }, true);
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { SyncRequestType } from 'src/enum';
|
||||||
|
import { SYNC_TYPES_ORDER } from 'src/services/sync.service';
|
||||||
|
|
||||||
|
describe('types', () => {
|
||||||
|
it('should have all the types in the ordering variable', () => {
|
||||||
|
for (const key in SyncRequestType) {
|
||||||
|
expect(SYNC_TYPES_ORDER).includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,179 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||||
|
it('should detect and sync the first user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
||||||
|
if (!user) {
|
||||||
|
expect.fail('First user should exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: user.deletedAt,
|
||||||
|
email: user.email,
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a soft deleted user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const deletedAt = new Date().toISOString();
|
||||||
|
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
||||||
|
const deleted = await getRepository('user').create(deletedUser);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: auth.user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt,
|
||||||
|
email: deleted.email,
|
||||||
|
id: deleted.id,
|
||||||
|
name: deleted.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [response[1].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user);
|
||||||
|
await userRepo.delete({ id: user.id }, true);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
type: 'UserDeleteV1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: auth.user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync a user and then an update to that same user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: auth.user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
||||||
|
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(updatedSyncResponse).toHaveLength(1);
|
||||||
|
expect(updatedSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: updated.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue