nc-photos/app/test/test_util.dart

667 lines
19 KiB
Dart
Raw Normal View History

import 'dart:convert';
2021-12-26 20:11:31 +01:00
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:drift/native.dart' as sql;
2022-07-15 20:14:12 +02:00
import 'package:equatable/equatable.dart';
2021-11-11 18:04:17 +01:00
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
2021-11-23 08:39:35 +01:00
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/exif.dart';
2021-11-11 18:04:17 +01:00
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
2021-11-11 18:04:17 +01:00
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:np_async/np_async.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db/np_db.dart';
import 'package:np_db_sqlite/np_db_sqlite.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:np_geocoder/np_geocoder.dart';
2023-08-25 18:37:17 +02:00
import 'package:np_string/np_string.dart';
2021-12-02 07:04:11 +01:00
part 'test_compat_util.dart';
2021-11-23 08:39:35 +01:00
class FilesBuilder {
FilesBuilder({
int initialFileId = 0,
}) : fileId = initialFileId;
List<File> build() {
return files.map((f) => f.copyWith()).toList();
}
2022-01-01 18:29:51 +01:00
void add(
2021-11-23 08:39:35 +01:00
String relativePath, {
2022-01-01 18:29:51 +01:00
int? contentLength,
String? contentType,
String? etag,
2021-11-23 08:39:35 +01:00
DateTime? lastModified,
2022-01-01 18:29:51 +01:00
bool isCollection = false,
2021-11-23 08:39:35 +01:00
bool hasPreview = true,
bool? isFavorite,
2021-11-23 08:39:35 +01:00
String ownerId = "admin",
String? ownerDisplayName,
Metadata? metadata,
ImageLocation? location,
2021-11-23 08:39:35 +01:00
}) {
2022-01-01 18:29:51 +01:00
files.add(File(
2021-11-23 08:39:35 +01:00
path: "remote.php/dav/files/$relativePath",
contentLength: contentLength,
2022-01-01 18:29:51 +01:00
contentType: contentType,
etag: etag,
2021-11-23 08:39:35 +01:00
lastModified:
lastModified ?? DateTime.utc(2020, 1, 2, 3, 4, 5 + files.length),
2022-01-01 18:29:51 +01:00
isCollection: isCollection,
2021-11-23 08:39:35 +01:00
hasPreview: hasPreview,
fileId: fileId++,
isFavorite: isFavorite,
2022-01-01 18:29:51 +01:00
ownerId: ownerId.toCi(),
ownerDisplayName: ownerDisplayName ?? ownerId.toString(),
metadata: metadata,
location: location,
2021-11-23 08:39:35 +01:00
));
}
2022-01-01 18:29:51 +01:00
void addGenericFile(
String relativePath,
String contentType, {
int contentLength = 1024,
String? etag,
2022-01-01 18:29:51 +01:00
DateTime? lastModified,
bool hasPreview = true,
bool? isFavorite,
2022-01-01 18:29:51 +01:00
String ownerId = "admin",
String? ownerDisplayName,
2022-01-01 18:29:51 +01:00
}) =>
add(
relativePath,
contentLength: contentLength,
contentType: contentType,
etag: etag,
2022-01-01 18:29:51 +01:00
lastModified: lastModified,
hasPreview: hasPreview,
isFavorite: isFavorite,
2022-01-01 18:29:51 +01:00
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
2022-01-01 18:29:51 +01:00
);
void addJpeg(
String relativePath, {
int contentLength = 1024,
String? etag,
2022-01-01 18:29:51 +01:00
DateTime? lastModified,
bool hasPreview = true,
bool? isFavorite,
2022-01-01 18:29:51 +01:00
String ownerId = "admin",
String? ownerDisplayName,
OrNull<Metadata>? metadata,
ImageLocation? location,
2022-01-01 18:29:51 +01:00
}) =>
add(
relativePath,
contentLength: contentLength,
contentType: "image/jpeg",
etag: etag,
2022-01-01 18:29:51 +01:00
lastModified: lastModified,
hasPreview: hasPreview,
isFavorite: isFavorite,
2022-01-01 18:29:51 +01:00
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
metadata: metadata == null
? Metadata(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
imageWidth: 640,
imageHeight: 480,
)
: metadata.obj,
location: location,
2022-01-01 18:29:51 +01:00
);
void addDir(
String relativePath, {
int contentLength = 1024,
String? etag,
2022-01-01 18:29:51 +01:00
DateTime? lastModified,
bool? isFavorite,
2022-01-01 18:29:51 +01:00
String ownerId = "admin",
String? ownerDisplayName,
2022-01-01 18:29:51 +01:00
}) =>
add(
relativePath,
etag: etag,
2022-01-01 18:29:51 +01:00
lastModified: lastModified,
isCollection: true,
hasPreview: false,
isFavorite: isFavorite,
2022-01-01 18:29:51 +01:00
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
2022-01-01 18:29:51 +01:00
);
void addAlbumJson(
String homeDir,
String filename, {
int contentLength = 1024,
String? etag,
DateTime? lastModified,
String ownerId = "admin",
String? ownerDisplayName,
}) =>
add(
"$homeDir/.com.nkming.nc_photos/albums/$filename.nc_album.json",
contentLength: contentLength,
contentType: "application/json",
etag: etag,
lastModified: lastModified,
hasPreview: false,
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
);
2021-11-23 08:39:35 +01:00
final files = <File>[];
int fileId;
}
/// Create an album for testing
class AlbumBuilder {
AlbumBuilder({
DateTime? lastUpdated,
2021-11-27 10:22:51 +01:00
String? name,
this.albumFilename = "test0.nc_album.json",
2021-11-23 08:39:35 +01:00
this.fileId = 0,
2021-11-27 10:22:51 +01:00
String? ownerId,
}) : lastUpdated = lastUpdated ?? DateTime.utc(2020, 1, 2, 3, 4, 5),
name = name ?? "test",
ownerId = ownerId ?? "admin";
factory AlbumBuilder.ofId({
required int albumId,
DateTime? lastUpdated,
String? name,
String? ownerId,
}) =>
AlbumBuilder(
lastUpdated: lastUpdated,
name: name,
albumFilename: "test$albumId.nc_album.json",
fileId: albumId,
ownerId: ownerId,
);
2021-11-23 08:39:35 +01:00
Album build() {
final latestFileItem = items
.whereType<AlbumFileItem>()
.stableSorted((a, b) => a.file.fdDateTime.compareTo(b.file.fdDateTime))
2021-11-23 08:39:35 +01:00
.reversed
.firstOrNull;
return Album(
lastUpdated: lastUpdated,
name: name,
provider: AlbumStaticProvider(
items: items,
latestItemTime: latestFileItem?.file.fdDateTime,
2021-11-23 08:39:35 +01:00
),
coverProvider: cover == null
? AlbumAutoCoverProvider(coverFile: latestFileItem?.file)
: AlbumManualCoverProvider(coverFile: cover!),
sortProvider: const AlbumNullSortProvider(),
shares: shares.isEmpty ? null : shares,
albumFile: buildAlbumFile(
path: buildAlbumFilePath(albumFilename, user: ownerId),
fileId: fileId,
ownerId: ownerId,
),
);
}
/// Add a file item
///
/// By default, the item will be added by admin and added at the same time as
/// the file's lastModified.
///
/// If [isCover] is true, the coverProvider of the album will become
/// [AlbumManualCoverProvider]
void addFileItem(
FileDescriptor file, {
2021-11-23 08:39:35 +01:00
String addedBy = "admin",
DateTime? addedAt,
bool isCover = false,
String? ownerId,
2021-11-23 08:39:35 +01:00
}) {
final fileItem = AlbumFileItem(
file: file,
addedBy: addedBy.toCi(),
addedAt: addedAt ?? file.fdDateTime,
ownerId: ownerId?.toCi() ?? file.as<File>()?.ownerId ?? addedBy.toCi(),
2021-11-23 08:39:35 +01:00
);
items.add(fileItem);
if (isCover) {
cover = file;
}
}
/// Add an album share
2021-11-24 09:36:16 +01:00
///
/// By default, the album will be shared at 2020-01-02 03:04:05
void addShare(
String userId, {
DateTime? sharedAt,
}) {
shares.add(buildAlbumShare(
userId: userId,
sharedAt: sharedAt,
));
2021-11-23 08:39:35 +01:00
}
2021-12-02 06:00:18 +01:00
static List<AlbumFileItem> fileItemsOf(Album album) =>
2021-11-23 08:39:35 +01:00
AlbumStaticProvider.of(album).items.whereType<AlbumFileItem>().toList();
final DateTime lastUpdated;
final String name;
final String albumFilename;
final int fileId;
final String ownerId;
final items = <AlbumItem>[];
FileDescriptor? cover;
2021-11-23 08:39:35 +01:00
final shares = <AlbumShare>[];
}
2021-11-11 18:04:17 +01:00
2022-07-15 20:14:12 +02:00
class SqlAccountWithServer with EquatableMixin {
const SqlAccountWithServer(this.server, this.account);
@override
get props => [server, account];
final compat.Server server;
final compat.Account account;
2022-07-15 20:14:12 +02:00
}
2021-11-11 18:04:17 +01:00
void initLog() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
2021-12-02 06:00:18 +01:00
String msg =
2022-08-06 19:00:46 +02:00
"[${record.loggerName}] ${record.level.name} ${record.time}: ${record.message}";
2021-12-02 06:00:18 +01:00
if (record.error != null) {
msg += " (throw: ${record.error.runtimeType} { ${record.error} })";
}
if (record.stackTrace != null) {
msg += "\nStack Trace:\n${record.stackTrace}";
}
2021-12-07 15:28:20 +01:00
int color;
if (record.level >= Level.SEVERE) {
color = 91;
} else if (record.level >= Level.WARNING) {
color = 33;
} else if (record.level >= Level.INFO) {
color = 34;
} else if (record.level >= Level.FINER) {
color = 32;
} else {
color = 90;
}
msg = "\x1B[${color}m$msg\x1B[0m";
2021-12-02 06:00:18 +01:00
debugPrint(msg);
2021-11-11 18:04:17 +01:00
});
}
Account buildAccount({
2024-02-13 12:29:13 +01:00
String? id,
2021-11-11 18:04:17 +01:00
String scheme = "http",
String address = "example.com",
String userId = "admin",
String username2 = "admin",
2021-11-11 18:04:17 +01:00
String password = "pass",
List<String> roots = const [""],
}) =>
Account(
2024-02-13 12:29:13 +01:00
id: id ?? "$userId-000000",
scheme: scheme,
address: address,
userId: userId.toCi(),
username2: username2,
password: password,
roots: roots,
);
2021-11-11 18:04:17 +01:00
/// Build a mock [File] pointing to a album JSON file
///
/// Warning: not all fields are filled, but the most essential ones are
File buildAlbumFile({
required String path,
int contentLength = 1024,
DateTime? lastModified,
required int fileId,
String ownerId = "admin",
String? ownerDisplayName,
2022-08-31 17:13:15 +02:00
String? etag,
2021-11-11 18:04:17 +01:00
}) =>
File(
path: path,
contentLength: contentLength,
contentType: "application/json",
lastModified: lastModified ?? DateTime.utc(2020, 1, 2, 3, 4, 5),
isCollection: false,
hasPreview: false,
fileId: fileId,
2021-11-12 22:13:02 +01:00
ownerId: ownerId.toCi(),
ownerDisplayName: ownerDisplayName ?? ownerId.toString(),
2022-08-31 17:13:15 +02:00
etag: fileId.toString(),
2021-11-11 18:04:17 +01:00
);
String buildAlbumFilePath(
String filename, {
String user = "admin",
}) =>
"remote.php/dav/files/$user/.com.nkming.nc_photos/albums/$filename";
2021-11-23 08:39:35 +01:00
AlbumShare buildAlbumShare({
required String userId,
String? displayName,
2021-11-24 09:36:16 +01:00
DateTime? sharedAt,
2021-11-23 08:39:35 +01:00
}) =>
AlbumShare(
userId: userId.toCi(),
displayName: displayName ?? userId,
2021-11-24 09:36:16 +01:00
sharedAt: sharedAt ?? DateTime.utc(2020, 1, 2, 3, 4, 5),
2021-11-23 08:39:35 +01:00
);
2021-11-11 18:04:17 +01:00
/// Build a mock [File] pointing to a JPEG image file
///
/// Warning: not all fields are filled, but the most essential ones are
File buildJpegFile({
required String path,
int contentLength = 1024,
DateTime? lastModified,
bool hasPreview = true,
required int fileId,
String ownerId = "admin",
String? ownerDisplayName,
2021-11-11 18:04:17 +01:00
}) =>
File(
path: path,
contentLength: contentLength,
contentType: "image/jpeg",
lastModified: lastModified ?? DateTime.utc(2020, 1, 2, 3, 4, 5),
isCollection: false,
hasPreview: hasPreview,
fileId: fileId,
2021-11-12 22:13:02 +01:00
ownerId: ownerId.toCi(),
ownerDisplayName: ownerDisplayName ?? ownerId.toString(),
2021-11-11 18:04:17 +01:00
);
FileDescriptor fileToFileDescriptor(File f) => FileDescriptor(
fdPath: f.path,
fdId: f.fileId!,
fdMime: f.contentType,
fdIsArchived: f.isArchived ?? false,
fdIsFavorite: f.isFavorite ?? false,
fdDateTime: f.bestDateTime,
);
2021-11-11 18:04:17 +01:00
Share buildShare({
required String id,
DateTime? stime,
String uidOwner = "admin",
String? displaynameOwner,
required File file,
required String shareWith,
}) =>
Share(
id: id,
shareType: ShareType.user,
stime: stime ?? DateTime.utc(2020, 1, 2, 3, 4, 5),
2021-11-12 22:13:02 +01:00
uidOwner: uidOwner.toCi(),
2021-11-11 18:04:17 +01:00
displaynameOwner: displaynameOwner ?? uidOwner,
uidFileOwner: file.ownerId!,
2021-11-11 18:04:17 +01:00
path: file.strippedPath,
itemType: ShareItemType.file,
mimeType: file.contentType ?? "",
itemSource: file.fileId!,
2021-11-12 22:13:02 +01:00
shareWith: shareWith.toCi(),
2021-11-11 18:04:17 +01:00
shareWithDisplayName: shareWith,
);
Sharee buildSharee({
ShareeType type = ShareeType.user,
2021-11-23 08:39:35 +01:00
String? label,
2021-11-11 18:04:17 +01:00
int shareType = 0,
2021-11-12 22:13:02 +01:00
required CiString shareWith,
2021-11-11 18:04:17 +01:00
String? shareWithDisplayNameUnique,
}) =>
Sharee(
type: type,
2021-11-23 08:39:35 +01:00
label: label ?? shareWith.toString(),
2021-11-11 18:04:17 +01:00
shareType: shareType,
shareWith: shareWith,
);
2021-12-02 07:04:11 +01:00
NpDb buildTestDb() {
final db = NpDbSqlite();
db.initWithDb(
db: compat.SqliteDb(
executor: sql.NativeDatabase.memory(
logStatements: true,
),
),
);
sql.driftRuntimeOptions.debugPrint = _debugPrintSql;
return db;
2021-12-02 07:04:11 +01:00
}
Future<void> insertFiles(
compat.SqliteDb db, Account account, Iterable<File> files) async {
final dbAccount = await db.accountOf(compat.ByAccount.db(account.toDb()));
for (final f in files) {
final sharedQuery = db.selectOnly(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId),
useColumns: false),
])
..addColumns([db.files.rowId])
..where(db.accountFiles.account.equals(dbAccount.rowId).not())
..where(db.files.fileId.equals(f.fileId!));
var rowId = (await sharedQuery.map((r) => r.read(db.files.rowId)).get())
.firstOrNull;
final insert = _SqliteFileConverter.toSql(dbAccount, f);
if (rowId == null) {
final dbFile = await db.into(db.files).insertReturning(insert.file);
rowId = dbFile.rowId;
}
final dbAccountFile = await db
.into(db.accountFiles)
.insertReturning(insert.accountFile.copyWith(file: sql.Value(rowId)));
if (insert.image != null) {
await db.into(db.images).insert(
insert.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
}
if (insert.imageLocation != null) {
await db.into(db.imageLocations).insert(insert.imageLocation!
.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
}
if (insert.trash != null) {
await db
.into(db.trashes)
.insert(insert.trash!.copyWith(file: sql.Value(rowId)));
}
}
}
Future<void> insertDirRelation(compat.SqliteDb db, Account account, File dir,
Iterable<File> children) async {
final dbAccount = await db.accountOf(compat.ByAccount.db(account.toDb()));
final dirRowIds = (await db
.accountFileRowIdsByFileIds(_ByAccount.sql(dbAccount), [dir.fileId!]))
.first;
final childRowIds = await db.accountFileRowIdsByFileIds(
_ByAccount.sql(dbAccount), [dir, ...children].map((f) => f.fileId!));
await db.batch((batch) {
batch.insertAll(
db.dirFiles,
childRowIds.map((c) => compat.DirFilesCompanion.insert(
dir: dirRowIds.fileRowId,
child: c.fileRowId,
)),
);
});
}
Future<void> insertAlbums(
compat.SqliteDb db, Account account, Iterable<Album> albums) async {
final dbAccount = await db.accountOf(compat.ByAccount.db(account.toDb()));
for (final a in albums) {
final rowIds =
await db.accountFileRowIdsOf(a.albumFile!, sqlAccount: dbAccount);
2022-08-31 17:13:15 +02:00
final insert =
_SqliteAlbumConverter.toSql(a, rowIds.fileRowId, a.albumFile!.etag!);
final dbAlbum = await db.into(db.albums).insertReturning(insert.album);
for (final s in insert.shares) {
await db
.into(db.albumShares)
.insert(s.copyWith(album: sql.Value(dbAlbum.rowId)));
}
}
}
Future<Set<File>> listSqliteDbFiles(compat.SqliteDb db) async {
final query = db.select(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
sql.innerJoin(
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
sql.leftOuterJoin(
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
sql.leftOuterJoin(db.imageLocations,
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
]);
return (await query
.map((r) => _SqliteFileConverter.fromSql(
r.readTable(db.accounts).userId,
compat.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
r.readTableOrNull(db.imageLocations),
r.readTableOrNull(db.trashes),
),
))
.get())
.toSet();
}
Future<Map<File, Set<File>>> listSqliteDbDirs(compat.SqliteDb db) async {
final query = db.select(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
sql.innerJoin(
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
sql.leftOuterJoin(
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
sql.leftOuterJoin(db.imageLocations,
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
]);
final fileMap = Map.fromEntries(await query.map((r) {
final f = compat.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
r.readTableOrNull(db.imageLocations),
r.readTableOrNull(db.trashes),
);
return MapEntry(
f.file.rowId,
_SqliteFileConverter.fromSql(r.readTable(db.accounts).userId, f),
);
}).get());
final dirQuery = db.select(db.dirFiles);
2024-06-30 21:00:11 +02:00
final dirs =
await dirQuery.map((r) => (dirRowId: r.dir, childRowId: r.child)).get();
final result = <File, Set<File>>{};
2024-06-30 21:00:11 +02:00
for (final (:dirRowId, :childRowId) in dirs) {
(result[fileMap[dirRowId]!] ??= <File>{}).add(fileMap[childRowId]!);
}
return result;
}
Future<Set<Album>> listSqliteDbAlbums(compat.SqliteDb db) async {
final albumQuery = db.select(db.albums).join([
sql.innerJoin(db.files, db.files.rowId.equalsExp(db.albums.file)),
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
sql.innerJoin(
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
]);
final albums = await albumQuery.map((r) {
final albumFile = _SqliteFileConverter.fromSql(
r.readTable(db.accounts).userId,
compat.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
null,
null,
null,
),
);
2024-06-30 21:00:11 +02:00
return (
rowId: r.read(db.albums.rowId)!,
album:
_SqliteAlbumConverter.fromSql(r.readTable(db.albums), albumFile, []),
);
}).get();
final results = <Album>{};
2024-06-30 21:00:11 +02:00
for (final (:rowId, :album) in albums) {
final shareQuery = db.select(db.albumShares)
2024-06-30 21:00:11 +02:00
..where((t) => t.album.equals(rowId));
final dbShares = await shareQuery.get();
2024-06-30 21:00:11 +02:00
results.add(album.copyWith(
lastUpdated: const OrNull(null),
shares: dbShares.isEmpty
? null
: OrNull(dbShares
.map((s) => AlbumShare(
2024-06-30 21:00:11 +02:00
userId: s.userId.toCi(),
displayName: s.displayName,
sharedAt: s.sharedAt,
))
.toList()),
));
}
return results;
}
2022-07-15 20:14:12 +02:00
Future<Set<SqlAccountWithServer>> listSqliteDbServerAccounts(
compat.SqliteDb db) async {
2022-07-15 20:14:12 +02:00
final query = db.select(db.servers).join([
sql.leftOuterJoin(
db.accounts, db.accounts.server.equalsExp(db.servers.rowId)),
]);
return (await query
.map((r) => SqlAccountWithServer(
r.readTable(db.servers), r.readTable(db.accounts)))
.get())
.toSet();
}
bool shouldPrintSql = false;
void _debugPrintSql(String log) {
if (shouldPrintSql) {
debugPrint(log, wrapWidth: 1024);
}
}