mirror of
https://github.com/immich-app/immich.git
synced 2026-07-23 05:44:48 +03:00
Compare commits
2 Commits
fix/partne
...
fix/auto-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5302d6ea0e | ||
|
|
564cda5088 |
@@ -6,6 +6,7 @@ const Map<String, Locale> locales = {
|
||||
// Additional locales
|
||||
'Arabic (ar)': Locale('ar'),
|
||||
'Basque (eu)': Locale('eu'),
|
||||
'Belarusian (be)': Locale('be'),
|
||||
'Bosnian (bl)': Locale('bn'),
|
||||
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
|
||||
'Bulgarian (bg)': Locale('bg'),
|
||||
|
||||
@@ -24,19 +24,13 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
const DriftLocalAssetRepository(this._db) : super(_db);
|
||||
|
||||
SingleOrNullSelectable<LocalAsset?> _assetSelectable(String id) {
|
||||
final query =
|
||||
_db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) &
|
||||
_db.remoteAssetEntity.ownerId.isInQuery(
|
||||
_db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id]),
|
||||
),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.localAssetEntity.id.equals(id))
|
||||
..limit(1);
|
||||
final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
])..where(_db.localAssetEntity.id.equals(id));
|
||||
|
||||
return query.map((row) {
|
||||
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||
|
||||
@@ -59,13 +59,7 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getByChecksum(String checksum) {
|
||||
final query = _db.remoteAssetEntity.select()
|
||||
..where(
|
||||
(row) =>
|
||||
row.checksum.equals(checksum) &
|
||||
row.ownerId.isInQuery(_db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id])),
|
||||
)
|
||||
..limit(1);
|
||||
final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum));
|
||||
|
||||
return query.map((row) => row.toDto()).getSingleOrNull();
|
||||
}
|
||||
|
||||
@@ -136,6 +136,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..addColumns([assetCountExp, dateExp])
|
||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||
@@ -159,10 +164,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) &
|
||||
_db.remoteAssetEntity.ownerId.isInQuery(
|
||||
_db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id]),
|
||||
),
|
||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
|
||||
@@ -16,11 +16,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
|
||||
const DriftTrashedLocalAssetRepository(this._db) : super(_db);
|
||||
|
||||
/// Matches remote_asset_entity rows owned by the current user. The asset is unique over (owner, checksum),
|
||||
/// so partners can have a duplicate checksum
|
||||
Expression<bool> get _ownedByCurrentUser =>
|
||||
_db.remoteAssetEntity.ownerId.isInQuery(_db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id]));
|
||||
|
||||
Future<void> updateHashes(Map<String, String> hashes) {
|
||||
if (hashes.isEmpty) {
|
||||
return Future.value();
|
||||
@@ -50,7 +45,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
await (_db.select(_db.trashedLocalAssetEntity).join([
|
||||
innerJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum) & _ownedByCurrentUser,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum),
|
||||
),
|
||||
])..where(
|
||||
_db.trashedLocalAssetEntity.source.equalsValue(TrashOrigin.remoteSync) &
|
||||
@@ -278,7 +273,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum) & _ownedByCurrentUser,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||
),
|
||||
])..where(
|
||||
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
||||
|
||||
@@ -10,6 +10,24 @@ String sanitizeUrl(String url) {
|
||||
return urlWithSchema.trimRight().replaceFirst(RegExp(r"/+$"), "");
|
||||
}
|
||||
|
||||
/// Validates a user-entered server URL
|
||||
bool isValidServerUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!url.contains('://')) {
|
||||
// Prepend conforming scheme for URL validation
|
||||
// Uri.tryParse will not parse out host without a scheme provided, even though we assume http(s) when connecting
|
||||
url = 'http://$url';
|
||||
}
|
||||
|
||||
final parsedUrl = Uri.tryParse(url);
|
||||
return parsedUrl != null &&
|
||||
(parsedUrl.scheme.isEmpty || parsedUrl.scheme.startsWith(RegExp(r'https?'))) &&
|
||||
parsedUrl.host.isNotEmpty;
|
||||
}
|
||||
|
||||
String? getServerUrl() {
|
||||
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
|
||||
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||
|
||||
@@ -43,18 +43,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
|
||||
final log = Logger('LoginForm');
|
||||
|
||||
String? _validateUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsedUrl = Uri.tryParse(url);
|
||||
if (parsedUrl == null || !parsedUrl.isAbsolute || !parsedUrl.scheme.startsWith("http") || parsedUrl.host.isEmpty) {
|
||||
return 'login_form_err_invalid_url'.tr();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
String? _validateUrl(String? url) => isValidServerUrl(url) ? null : 'login_form_err_invalid_url'.tr();
|
||||
|
||||
String? _validateEmail(String? email) {
|
||||
if (email == null || email == '') {
|
||||
|
||||
@@ -19,72 +19,6 @@ void main() {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('get', () {
|
||||
late String userId;
|
||||
|
||||
setUp(() async {
|
||||
final user = await ctx.newUser();
|
||||
userId = user.id;
|
||||
// Owner-scoped queries resolve the current user via authUserEntity.
|
||||
await ctx.newAuthUser(id: userId);
|
||||
});
|
||||
|
||||
test('allows the same checksum to exist for multiple owners (#29973)', () async {
|
||||
const checksum = 'some-shared-checksum';
|
||||
final mine = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
final partner = await ctx.newUser();
|
||||
await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum);
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
|
||||
final result = await sut.get(local.id);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.id, local.id);
|
||||
// We must explicitly get OUR asset, not the partner's
|
||||
expect(result.remoteId, mine.id);
|
||||
});
|
||||
|
||||
test('reports local-only when only a partner has a remote copy (#29973)', () async {
|
||||
// The current user has NOT uploaded this file; only a partner owns an identical-checksum remote asset
|
||||
const checksum = 'partner-only';
|
||||
final partner = await ctx.newUser();
|
||||
await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum);
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
|
||||
final result = await sut.get(local.id);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.remoteId, isNull);
|
||||
expect(result.storage, AssetState.local);
|
||||
});
|
||||
|
||||
test('allows the current user to have multiple remote rows for one checksum (#29973)', () async {
|
||||
// A single user can have many remote assets with the same checksum (their upload + external library copy)
|
||||
const checksum = 'multi-library';
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
|
||||
final result = await sut.get(local.id);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.id, local.id);
|
||||
expect(result.remoteId, isNotNull);
|
||||
});
|
||||
|
||||
test('attaches remoteId to local asset automatically in simple scenarios', () async {
|
||||
const checksum = 'simple';
|
||||
final remote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
|
||||
final result = await sut.get(local.id);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.remoteId, remote.id);
|
||||
expect(result.storage, AssetState.merged);
|
||||
});
|
||||
});
|
||||
|
||||
group('getRemovalCandidates', () {
|
||||
final cutoffDate = DateTime(2024, 1, 1);
|
||||
final beforeCutoff = DateTime(2023, 12, 31);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late RemoteAssetRepository sut;
|
||||
|
||||
setUp(() {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = RemoteAssetRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getByChecksum', () {
|
||||
late String userId;
|
||||
|
||||
setUp(() async {
|
||||
final user = await ctx.newUser();
|
||||
userId = user.id;
|
||||
await ctx.newAuthUser(id: userId);
|
||||
});
|
||||
|
||||
test('returns the current user\'s asset when a partner shares the checksum', () async {
|
||||
const checksum = 'shared-partner-checksum';
|
||||
final mine = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
final partner = await ctx.newUser();
|
||||
await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum);
|
||||
|
||||
final result = await sut.getByChecksum(checksum);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.id, mine.id);
|
||||
expect(result.ownerId, userId);
|
||||
});
|
||||
|
||||
test('returns null when current user does not own the checksum, but a partner does', () async {
|
||||
const checksum = 'partner-only';
|
||||
final partner = await ctx.newUser();
|
||||
await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum);
|
||||
|
||||
final result = await sut.getByChecksum(checksum);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns the current user\'s asset', () async {
|
||||
const checksum = 'simple';
|
||||
final remote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
|
||||
final result = await sut.getByChecksum(checksum);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.id, remote.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -102,48 +102,4 @@ void main() {
|
||||
expect(remote.localId, local.id);
|
||||
});
|
||||
});
|
||||
|
||||
group('localAlbum assets', () {
|
||||
late String userId;
|
||||
late String otherUserId;
|
||||
|
||||
setUp(() async {
|
||||
final user = await ctx.newUser();
|
||||
userId = user.id;
|
||||
await ctx.newAuthUser(id: userId);
|
||||
final other = await ctx.newUser();
|
||||
otherUserId = other.id;
|
||||
});
|
||||
|
||||
test('does not duplicate assets when a partner shares the checksum', () async {
|
||||
const checksum = 'shared-partner-checksum';
|
||||
final album = await ctx.newLocalAlbum();
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id);
|
||||
final myRemote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
await ctx.newRemoteAsset(ownerId: otherUserId, checksum: checksum);
|
||||
|
||||
final assets = await sut.localAlbum(album.id, .day).assetSource(0, 10);
|
||||
|
||||
expect(assets, hasLength(1));
|
||||
final asset = assets.single as LocalAsset;
|
||||
expect(asset.id, local.id);
|
||||
// Must resolve the current user's remote id
|
||||
expect(asset.remoteId, myRemote.id);
|
||||
});
|
||||
|
||||
test('bucket count ignores a partner sharing the checksum', () async {
|
||||
const checksum = 'shared-partner-checksum';
|
||||
final album = await ctx.newLocalAlbum();
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id);
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
await ctx.newRemoteAsset(ownerId: otherUserId, checksum: checksum);
|
||||
|
||||
final buckets = await sut.localAlbum(album.id, .day).bucketSource().first;
|
||||
|
||||
expect(buckets, hasLength(1));
|
||||
expect(buckets.single.assetCount, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late DriftTrashedLocalAssetRepository sut;
|
||||
|
||||
setUp(() {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = DriftTrashedLocalAssetRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getToRestore', () {
|
||||
late String userId;
|
||||
|
||||
setUp(() async {
|
||||
final user = await ctx.newUser();
|
||||
userId = user.id;
|
||||
await ctx.newAuthUser(id: userId);
|
||||
});
|
||||
|
||||
test('does not restore based on a partner\'s live remote copy', () async {
|
||||
const checksum = 'shared-partner-checksum';
|
||||
final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final trashed = await ctx.newTrashedLocalAsset(albumId: album.id, checksum: checksum);
|
||||
|
||||
// Current user's own remote copy is deleted; only a partner has an active copy.
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum, deletedAt: DateTime(2020, 1, 1));
|
||||
final partner = await ctx.newUser();
|
||||
await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum);
|
||||
|
||||
final result = await sut.getToRestore();
|
||||
|
||||
final ids = result.map((a) => a.id);
|
||||
expect(ids, isNot(contains(trashed.id)));
|
||||
});
|
||||
|
||||
test('restores when the current user\'s own remote copy is live', () async {
|
||||
const checksum = 'my-live-copy';
|
||||
final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final trashed = await ctx.newTrashedLocalAsset(albumId: album.id, checksum: checksum);
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
|
||||
final result = await sut.getToRestore();
|
||||
|
||||
final ids = result.map((a) => a.id);
|
||||
expect(ids, contains(trashed.id));
|
||||
});
|
||||
});
|
||||
|
||||
group('getToTrash', () {
|
||||
late String userId;
|
||||
|
||||
setUp(() async {
|
||||
final user = await ctx.newUser();
|
||||
userId = user.id;
|
||||
await ctx.newAuthUser(id: userId);
|
||||
});
|
||||
|
||||
Future<String> addLocalAssetToBackupAlbum(String checksum) async {
|
||||
final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id);
|
||||
return local.id;
|
||||
}
|
||||
|
||||
test('does not trash when only a partner\'s remote copy is deleted', () async {
|
||||
const checksum = 'shared-partner-checksum';
|
||||
final localId = await addLocalAssetToBackupAlbum(checksum);
|
||||
|
||||
// Current user's own remote copy is live but a partner deleted theirs
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum);
|
||||
final partner = await ctx.newUser();
|
||||
await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum, deletedAt: DateTime(2020, 1, 1));
|
||||
|
||||
final result = await sut.getToTrash();
|
||||
|
||||
final ids = result.values.expand((assets) => assets).map((a) => a.id);
|
||||
expect(ids, isNot(contains(localId)));
|
||||
});
|
||||
|
||||
test('trashes when the current user\'s own remote copy is deleted', () async {
|
||||
const checksum = 'my-deleted-copy';
|
||||
final localId = await addLocalAssetToBackupAlbum(checksum);
|
||||
await ctx.newRemoteAsset(ownerId: userId, checksum: checksum, deletedAt: DateTime(2020, 1, 1));
|
||||
|
||||
final result = await sut.getToTrash();
|
||||
|
||||
final ids = result.values.expand((assets) => assets).map((a) => a.id);
|
||||
expect(ids, contains(localId));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
@@ -19,8 +18,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
@@ -75,22 +72,6 @@ class MediumRepositoryContext {
|
||||
);
|
||||
}
|
||||
|
||||
/// Seeds a user into `authUserEntity` as the currently authenticated user
|
||||
Future<AuthUserEntityData> newAuthUser({String? id, String? email, bool? isAdmin, AvatarColor? avatarColor}) async {
|
||||
id ??= TestUtils.uuid();
|
||||
return db
|
||||
.into(db.authUserEntity)
|
||||
.insertReturning(
|
||||
AuthUserEntityCompanion(
|
||||
id: .new(id),
|
||||
email: .new(email ?? '$id@test.com'),
|
||||
name: .new('user_$id'),
|
||||
isAdmin: .new(isAdmin ?? false),
|
||||
avatarColor: .new(avatarColor ?? TestUtils.randElement(AvatarColor.values)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> newPartner({required String sharedById, required String sharedWithId, bool? inTimeline}) {
|
||||
return db
|
||||
.into(db.partnerEntity)
|
||||
@@ -305,33 +286,6 @@ class MediumRepositoryContext {
|
||||
);
|
||||
}
|
||||
|
||||
/// Seeds a trashed local asset into `trashedLocalAssetEntity`
|
||||
Future<TrashedLocalAssetEntityData> newTrashedLocalAsset({
|
||||
String? id,
|
||||
required String albumId,
|
||||
String? checksum,
|
||||
TrashOrigin? source,
|
||||
AssetType? type,
|
||||
DateTime? createdAt,
|
||||
bool? isFavorite,
|
||||
}) async {
|
||||
id ??= TestUtils.uuid();
|
||||
return db
|
||||
.into(db.trashedLocalAssetEntity)
|
||||
.insertReturning(
|
||||
TrashedLocalAssetEntityCompanion(
|
||||
id: .new(id),
|
||||
albumId: .new(albumId),
|
||||
name: .new('trashed_$id.jpg'),
|
||||
type: .new(type ?? .image),
|
||||
checksum: .new(checksum),
|
||||
source: .new(source ?? TrashOrigin.remoteSync),
|
||||
isFavorite: .new(isFavorite ?? false),
|
||||
createdAt: .new(TestUtils.date(createdAt)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<LocalAlbumEntityData> newLocalAlbum({
|
||||
String? id,
|
||||
String? name,
|
||||
|
||||
@@ -77,6 +77,40 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidServerUrl', () {
|
||||
test('should treat null as valid', () {
|
||||
expect(isValidServerUrl(null), isTrue);
|
||||
});
|
||||
|
||||
test('should treat empty string as valid', () {
|
||||
expect(isValidServerUrl(''), isTrue);
|
||||
});
|
||||
|
||||
test('should accept a bare host', () {
|
||||
expect(isValidServerUrl('demo.immich.app'), isTrue);
|
||||
});
|
||||
|
||||
test('should accept a bare host with a port', () {
|
||||
expect(isValidServerUrl('192.168.1.1:2283'), isTrue);
|
||||
});
|
||||
|
||||
test('should accept an http URL', () {
|
||||
expect(isValidServerUrl('http://demo.immich.app'), isTrue);
|
||||
});
|
||||
|
||||
test('should accept an https URL', () {
|
||||
expect(isValidServerUrl('https://demo.immich.app:2283/api'), isTrue);
|
||||
});
|
||||
|
||||
test('should reject a non-http scheme', () {
|
||||
expect(isValidServerUrl('ftp://demo.immich.app'), isFalse);
|
||||
});
|
||||
|
||||
test('should reject scheme only input', () {
|
||||
expect(isValidServerUrl('https://'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('punycodeDecodeUrl', () {
|
||||
test('should return null for null input', () {
|
||||
expect(punycodeDecodeUrl(null), isNull);
|
||||
|
||||
Reference in New Issue
Block a user