diff --git a/app/test/entity/sqlite_table_extension_test.dart b/app/test/entity/sqlite_table_extension_test.dart new file mode 100644 index 00000000..648a9e36 --- /dev/null +++ b/app/test/entity/sqlite_table_extension_test.dart @@ -0,0 +1,279 @@ +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/sqlite_table.dart' as sql; +import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; +import 'package:nc_photos/object_extension.dart'; +import 'package:test/test.dart'; + +import '../test_util.dart' as util; + +void main() { + group("SqliteDbExtension", () { + group("insertAccountOf", () { + test("first", _insertAccountFirst); + test("same server", _insertAccountSameServer); + test("same account", _insertAccountSameAccount); + }); + group("deleteAccountOf", () { + test("normal", _deleteAccount); + test("same server", _deleteAccountSameServer); + test("same server shared file", _deleteAccountSameServerSharedFile); + }); + test("cleanUpDanglingFiles", _cleanUpDanglingFiles); + }); +} + +/// Insert an Account to a empty db +/// +/// Expect: Account and Server inserted +Future _insertAccountFirst() async { + final account = util.buildAccount(); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + + await c.sqliteDb.use((db) async { + await db.insertAccountOf(account); + }); + expect( + await util.listSqliteDbServerAccounts(c.sqliteDb), + { + util.SqlAccountWithServer( + sql.Server(rowId: 1, address: "http://example.com"), + sql.Account(rowId: 1, server: 1, userId: "admin"), + ), + }, + ); +} + +/// Insert an Account with Server already exists in db +/// +/// Expect: Account and Server inserted +Future _insertAccountSameServer() async { + final account = util.buildAccount(); + final user1Account = util.buildAccount(userId: "user1"); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccountOf(account); + }); + + await c.sqliteDb.use((db) async { + await db.insertAccountOf(user1Account); + }); + expect( + await util.listSqliteDbServerAccounts(c.sqliteDb), + { + util.SqlAccountWithServer( + sql.Server(rowId: 1, address: "http://example.com"), + sql.Account(rowId: 1, server: 1, userId: "admin"), + ), + util.SqlAccountWithServer( + sql.Server(rowId: 1, address: "http://example.com"), + sql.Account(rowId: 2, server: 1, userId: "user1"), + ), + }, + ); +} + +/// Insert an Account with the same info as another entry +/// +/// Expect: Account not inserted +Future _insertAccountSameAccount() async { + final account = util.buildAccount(); + final account2 = util.buildAccount(); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccountOf(account); + }); + + await c.sqliteDb.use((db) async { + await db.insertAccountOf(account2); + }); + expect( + await util.listSqliteDbServerAccounts(c.sqliteDb), + { + util.SqlAccountWithServer( + sql.Server(rowId: 1, address: "http://example.com"), + sql.Account(rowId: 1, server: 1, userId: "admin"), + ), + }, + ); +} + +/// Delete Account +/// +/// Expect: Account deleted; +/// Server deleted; +/// Associated Files deleted +Future _deleteAccount() async { + final account = util.buildAccount(); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addJpeg("admin/test1.jpg")) + .build(); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccountOf(account); + await util.insertFiles(c.sqliteDb, account, files); + }); + + await c.sqliteDb.use((db) async { + await db.deleteAccountOf(account); + }); + expect( + await util.listSqliteDbServerAccounts(c.sqliteDb), + {}, + ); + expect( + await util.listSqliteDbFiles(c.sqliteDb), + {}, + ); +} + +/// Delete an Account having the same Server as other Accounts +/// +/// Expect: Account deleted; +/// Server remained; +/// Associated Files deleted +Future _deleteAccountSameServer() async { + final account = util.buildAccount(); + final user1Account = util.buildAccount(userId: "user1"); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addJpeg("admin/test1.jpg")) + .build(); + final user1Files = (util.FilesBuilder(initialFileId: files.length) + ..addDir("user1", ownerId: "user1") + ..addJpeg("user1/test2.jpg", ownerId: "user1")) + .build(); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccountOf(account); + await c.sqliteDb.insertAccountOf(user1Account); + await util.insertFiles(c.sqliteDb, account, files); + await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); + + await util.insertFiles(c.sqliteDb, user1Account, user1Files); + await util.insertDirRelation( + c.sqliteDb, user1Account, user1Files[0], [user1Files[1]]); + }); + + await c.sqliteDb.use((db) async { + await db.deleteAccountOf(account); + }); + expect( + await util.listSqliteDbServerAccounts(c.sqliteDb), + { + util.SqlAccountWithServer( + sql.Server(rowId: 1, address: "http://example.com"), + sql.Account(rowId: 2, server: 1, userId: "user1"), + ), + }, + ); + expect( + await util.listSqliteDbFiles(c.sqliteDb), + {...user1Files}, + ); +} + +/// Delete an Account having the same Server as other Accounts and with files +/// shared between them (i.e., 1 Files to many AccountFiles) +/// +/// Expect: Account deleted; +/// Server remained; +/// Associated Shared Files not deleted; +Future _deleteAccountSameServerSharedFile() async { + final account = util.buildAccount(); + final user1Account = util.buildAccount(userId: "user1"); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addJpeg("admin/test1.jpg")) + .build(); + final user1Files = (util.FilesBuilder(initialFileId: files.length) + ..addDir("user1", ownerId: "user1")) + .build(); + user1Files + .add(files[0].copyWith(path: "remote.php/dav/files/user1/test1.jpg")); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccountOf(account); + await c.sqliteDb.insertAccountOf(user1Account); + await util.insertFiles(c.sqliteDb, account, files); + await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); + + await util.insertFiles(c.sqliteDb, user1Account, user1Files); + await util.insertDirRelation( + c.sqliteDb, user1Account, user1Files[0], [user1Files[1]]); + }); + + await c.sqliteDb.use((db) async { + await db.deleteAccountOf(account); + }); + expect( + await util.listSqliteDbServerAccounts(c.sqliteDb), + { + util.SqlAccountWithServer( + sql.Server(rowId: 1, address: "http://example.com"), + sql.Account(rowId: 2, server: 1, userId: "user1"), + ), + }, + ); + expect( + await util.listSqliteDbFiles(c.sqliteDb), + {...user1Files}, + ); +} + +/// Clean up Files without an associated entry in AccountFiles +/// +/// Expect: Dangling files deleted +Future _cleanUpDanglingFiles() async { + final account = util.buildAccount(); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addJpeg("admin/test1.jpg")) + .build(); + final c = DiContainer( + sqliteDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccountOf(account); + await util.insertFiles(c.sqliteDb, account, files); + + await c.sqliteDb.applyFuture((db) async { + await db.into(db.files).insert(sql.FilesCompanion.insert( + server: 1, + fileId: files.length, + )); + }); + }); + + expect( + await c.sqliteDb.select(c.sqliteDb.files).map((f) => f.fileId).get(), + [0, 1, 2], + ); + await c.sqliteDb.use((db) async { + await db.cleanUpDanglingFiles(); + }); + expect( + await c.sqliteDb.select(c.sqliteDb.files).map((f) => f.fileId).get(), + [0, 1], + ); +} diff --git a/app/test/test_util.dart b/app/test/test_util.dart index feb23067..c808b7cc 100644 --- a/app/test/test_util.dart +++ b/app/test/test_util.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart' as sql; import 'package:drift/native.dart' as sql; +import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; @@ -241,6 +242,16 @@ class AlbumBuilder { final shares = []; } +class SqlAccountWithServer with EquatableMixin { + const SqlAccountWithServer(this.server, this.account); + + @override + get props => [server, account]; + + final sql.Server server; + final sql.Account account; +} + void initLog() { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { @@ -557,6 +568,19 @@ Future> listSqliteDbAlbums(sql.SqliteDb db) async { return results; } +Future> listSqliteDbServerAccounts( + sql.SqliteDb db) async { + 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) {