@TestOn('windows') library; import 'dart:ffi'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqlite3/open.dart'; import 'package:pyramid/core/app_database.dart'; /// Diese Tests fahren die ECHTE Klartext→SQLCipher-Migration – mit derselben /// SQLCipher-Bibliothek, die auch die App benutzt: der vom /// sqlcipher_flutter_libs-Plugin gebauten `sqlite3.dll` aus dem Windows-Build. /// Existiert kein Build-Output (z. B. auf dem Pi), werden sie übersprungen – /// dann zählt der Lauf als UNGETESTET im Sinne von CLAUDE.md. String? _findCipherDll() { for (final config in ['Debug', 'Profile', 'Release']) { final file = File('build/windows/x64/runner/$config/sqlite3.dll'); if (file.existsSync()) return file.absolute.path; } return null; } const _hexKey = 'a3b1c2d3e4f50617a3b1c2d3e4f50617a3b1c2d3e4f50617a3b1c2d3e4f50617'; void main() { final dllPath = _findCipherDll(); final skip = dllPath == null ? 'SQLCipher-DLL fehlt (erst "flutter build windows" bzw. ' '"flutter run -d windows" ausführen)' : null; // noIsolate: der Override muss im selben Isolate wirken wie die DB-Zugriffe. final factory = dllPath == null ? null : createDatabaseFactoryFfi( ffiInit: () { open.overrideFor( OperatingSystem.windows, () => DynamicLibrary.open(dllPath), ); }, noIsolate: true, ); late Directory tempDir; setUp(() async { tempDir = await Directory.systemTemp.createTemp('pyramid_db_test'); }); tearDown(() async { try { await tempDir.delete(recursive: true); } catch (_) {} }); /// Legt eine Klartext-DB mit zwei Tabellen und bekannten Zeilen an. Future createPlaintextDb() async { final dbPath = '${tempDir.path}${Platform.pathSeparator}pyramid.sqlite'; final db = await factory!.openDatabase(dbPath); await db.execute('CREATE TABLE box_client (k TEXT PRIMARY KEY, v TEXT)'); await db.execute( 'CREATE TABLE box_inbound_group_session (k TEXT PRIMARY KEY, v TEXT)', ); for (var i = 0; i < 50; i++) { await db.insert('box_client', {'k': 'key$i', 'v': 'wert$i'}); } for (var i = 0; i < 7; i++) { await db.insert( 'box_inbound_group_session', {'k': 'session$i', 'v': 'geheim$i'}, ); } await db.close(); return dbPath; } test('Geladene Bibliothek ist wirklich SQLCipher (cipher_version)', () async { final db = await factory!.openDatabase(inMemoryDatabasePath); final rows = await db.rawQuery('PRAGMA cipher_version'); await db.close(); expect(rows, isNotEmpty, reason: 'PRAGMA cipher_version leer → normales sqlite3 geladen'); expect(rows.first.values.first.toString(), isNotEmpty); }, skip: skip); test('Header-Erkennung: Klartext ja, verschlüsselt nein, Mini-Datei ja', () async { final dbPath = await createPlaintextDb(); expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isTrue); final tiny = File('${tempDir.path}${Platform.pathSeparator}tiny.sqlite'); await tiny.writeAsString('kurz'); expect(await AppDatabase.isPlaintextSqlite(tiny), isTrue); final encPath = '${tempDir.path}${Platform.pathSeparator}enc.sqlite'; final enc = await AppDatabase.openEncrypted(factory!, encPath, _hexKey); await enc.execute('CREATE TABLE t (x INTEGER)'); await enc.close(); expect(await AppDatabase.isPlaintextSqlite(File(encPath)), isFalse); }, skip: skip); test('Migration: verschlüsselt, alle Zeilen erhalten, Backup liegt da', () async { final dbPath = await createPlaintextDb(); final ok = await AppDatabase.migratePlaintextToEncrypted( factory: factory!, dbPath: dbPath, hexKey: _hexKey, ); expect(ok, isTrue); // Datei ist jetzt verschlüsselt, Aufräum-Artefakte sind weg. expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isFalse); expect(File('$dbPath.enc').existsSync(), isFalse); expect(File('$dbPath.migrating').existsSync(), isFalse); // Byte-Backup existiert und ist die alte Klartext-DB. final backup = File('$dbPath.premigration'); expect(backup.existsSync(), isTrue); expect(await AppDatabase.isPlaintextSqlite(backup), isTrue); // Inhalt vollständig? Mit Schlüssel öffnen und nachzählen. final db = await AppDatabase.openEncrypted(factory, dbPath, _hexKey); final clients = await db.rawQuery('SELECT count(*) AS c FROM box_client'); final sessions = await db .rawQuery('SELECT count(*) AS c FROM box_inbound_group_session'); final probe = await db.rawQuery( "SELECT v FROM box_inbound_group_session WHERE k = 'session3'"); await db.close(); expect(clients.first['c'], 50); expect(sessions.first['c'], 7); expect(probe.first['v'], 'geheim3'); }, skip: skip); test('Falscher Schlüssel scheitert sofort beim Öffnen', () async { final dbPath = await createPlaintextDb(); await AppDatabase.migratePlaintextToEncrypted( factory: factory!, dbPath: dbPath, hexKey: _hexKey, ); final wrongKey = 'ff${_hexKey.substring(2)}'; await expectLater( AppDatabase.openEncrypted(factory, dbPath, wrongKey), throwsA(anything), ); }, skip: skip); test('Fehlschlag lässt die Klartext-DB unangetastet weiterlaufen', () async { final dbPath = await createPlaintextDb(); // Fehler erzwingen: an der Stelle des Exports liegt ein VERZEICHNIS – // ATTACH kann die Datei nicht anlegen. await Directory('$dbPath.enc').create(); final ok = await AppDatabase.migratePlaintextToEncrypted( factory: factory!, dbPath: dbPath, hexKey: _hexKey, ); expect(ok, isFalse); // Original unversehrt, Marker weg, App könnte normal weiterlaufen. expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isTrue); expect(File('$dbPath.migrating').existsSync(), isFalse); final db = await factory.openDatabase(dbPath); final clients = await db.rawQuery('SELECT count(*) AS c FROM box_client'); await db.close(); expect(clients.first['c'], 50); }, skip: skip); }