Files
sepot/app/test/storage/app_data_test.dart
T
Julius Joska 8081d98c67 Šepot — end-to-end encrypted messenger
Initial public release. Anonymous on-device identity (no phone number,
e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double
Ratchet, sealed sender) via libsignal, a metadata-minimizing relay,
group chats, disappearing messages, encrypted local storage, optional
Tor transport, and a UI localized into Czech, English, German, Russian,
and Italian.

Monorepo: Flutter client (app/), Rust relay + wire types (rust/),
protobuf definitions (proto/), self-host deploy (deploy/).

Licensed under AGPL-3.0 with an app-store distribution exception (§7).
2026-07-11 17:21:47 +02:00

123 lines
3.9 KiB
Dart

import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:sepot/core/storage/crypto_db.dart';
import 'package:sepot/core/storage/secure_storage.dart';
import 'package:sepot/models/conversation.dart';
import 'package:sepot/models/message.dart';
/// S9: aplikační data (konverzace, zprávy, nastavení) žijí v SQLCipher DB,
/// ne v plain Hive. Testuje se DB vrstva i service nad ní (debugAttachDb —
/// OS keystore na Linux VM není).
void main() {
late Directory tmp;
late CryptoDb db;
late SecureStorageService storage;
final rng = Random.secure();
Uint8List randomDek() =>
Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256)));
setUp(() {
tmp = Directory.systemTemp.createTempSync('sepot_s9_test');
db = CryptoDb.open(path: '${tmp.path}/app.db', dek: randomDek());
storage = SecureStorageService(dbPath: '${tmp.path}/app.db')
..debugAttachDb(db);
});
tearDown(() {
db.close();
tmp.deleteSync(recursive: true);
});
test('zamčený storage nedá data — StateError, žádný tichý fallback', () {
final locked = SecureStorageService(dbPath: '${tmp.path}/locked.db');
expect(() => locked.db, throwsStateError);
expect(locked.isUnlocked, isFalse);
});
test('zprávy uložené přes addMessage přežijí reopen DB', () async {
final msg = Message.text(
senderId: 'alice',
recipientId: 'bob',
content: 'Tajná zpráva',
);
await storage.addMessage('bob', msg);
// reopen — data musí přijít z disku, ne z paměti
final path = '${tmp.path}/reopen.db';
final dek = randomDek();
final db1 = CryptoDb.open(path: path, dek: dek);
final s1 = SecureStorageService(dbPath: path)..debugAttachDb(db1);
await s1.addMessage('bob', msg);
db1.close();
final db2 = CryptoDb.open(path: path, dek: dek);
final s2 = SecureStorageService(dbPath: path)..debugAttachDb(db2);
final conv = await s2.loadConversation('bob');
expect(conv, isNotNull);
expect(conv!.messages.single.content, 'Tajná zpráva');
db2.close();
});
test('DB soubor neobsahuje plaintext obsahu zpráv', () async {
await storage.addMessage(
'bob',
Message.text(
senderId: 'alice',
recipientId: 'bob',
content: 'NAJDI-MELESS-TOKEN',
),
);
// WAL checkpoint, ať je obsah v hlavním souboru
db.raw.execute('PRAGMA wal_checkpoint(TRUNCATE);');
final bytes = File('${tmp.path}/app.db').readAsBytesSync();
expect(
String.fromCharCodes(bytes).contains('NAJDI-MELESS-TOKEN'),
isFalse,
reason: 'Obsah zprávy je na disku čitelný = at-rest šifrování neběží!',
);
});
test('konverzace se řadí podle poslední aktivity', () async {
await storage.saveConversation(
Conversation(
contactId: 'stary',
lastActivity: DateTime(2026, 1, 1),
),
);
await storage.saveConversation(
Conversation(
contactId: 'novy',
lastActivity: DateTime(2026, 7, 1),
),
);
final list = await storage.loadConversations();
expect(list.map((c) => c.contactId).toList(), ['novy', 'stary']);
});
test('smazání kontaktu smaže i jeho konverzaci (atomicky)', () async {
db.contactPut('bob', '{"id":"bob"}');
db.conversationPut('bob', '{"contactId":"bob","messages":[],'
'"lastActivity":"2026-07-03T00:00:00.000"}');
db.contactDelete('bob');
expect(db.contactsJsonAll(), isEmpty);
expect(db.conversationJson('bob'), isNull);
});
test('nastavení: JSON roundtrip přes app_settings', () async {
await storage.setSetting('disappearing_seconds', 86400);
await storage.setSetting('theme', 'dark');
expect(await storage.getSetting<int>('disappearing_seconds'), 86400);
expect(await storage.getSetting<String>('theme'), 'dark');
expect(await storage.getSetting<String>('neexistuje'), isNull);
});
}