// Tests for EnrollmentDb — verifies that users are created, listed, and // deleted correctly on the phone. // // All tests use an injected temp directory so path_provider is not needed. // // Run: flutter test test/enrollment_test.dart import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import '../lib/enrollment_db.dart'; void main() { late Directory tmp; late EnrollmentDb db; setUp(() async { tmp = await Directory.systemTemp.createTemp('enrollment_test_'); db = EnrollmentDb(baseDir: tmp); }); tearDown(() => tmp.delete(recursive: true)); // ------------------------------------------------------------------------- // Registration // ------------------------------------------------------------------------- group('register', () { test('creates probe-mode enrollment when no credential data provided', () async { final e = await db.register(username: 'alice', displayName: 'Alice Example'); expect(e.username, 'alice'); expect(e.displayName, 'Alice Example'); expect(e.hasCredential, isFalse); expect(e.credentialDataB64, isNull); expect(e.userIdB64, isNull); }); test('creates FIDO2 enrollment when credential data provided', () async { final e = await db.register( username: 'alice', userIdB64: 'dXNlcklk', credentialDataB64: 'Y3JlZERhdGE=', ); expect(e.hasCredential, isTrue); expect(e.credentialDataB64, 'Y3JlZERhdGE='); expect(e.userIdB64, 'dXNlcklk'); }); test('normalizes username to lowercase and trims whitespace', () async { final e = await db.register(username: ' BOB '); expect(e.username, 'bob'); }); test('sets createdAt and updatedAt to current time', () async { final before = DateTime.now().millisecondsSinceEpoch ~/ 1000; final e = await db.register(username: 'alice'); final after = DateTime.now().millisecondsSinceEpoch ~/ 1000; expect(e.createdAt, inInclusiveRange(before, after)); expect(e.updatedAt, e.createdAt); }); test('throws StateError on duplicate username', () async { await db.register(username: 'alice'); await expectLater( db.register(username: 'alice'), throwsA(isA()), ); }); test('duplicate check is case-insensitive', () async { await db.register(username: 'alice'); await expectLater( db.register(username: 'ALICE'), throwsA(isA()), ); }); test('rejects invalid username', () async { // Two-char usernames are rejected by the regex (1 or 3–32 chars only). await expectLater( db.register(username: 'ab'), throwsA(isA()), ); }); test('rejects username with special characters', () async { await expectLater( db.register(username: 'alice!'), throwsA(isA()), ); }); }); // ------------------------------------------------------------------------- // List // ------------------------------------------------------------------------- group('list', () { test('returns empty list when no users registered', () async { expect(await db.list(), isEmpty); }); test('returns all registered users sorted alphabetically', () async { await db.register(username: 'charlie'); await db.register(username: 'alice'); await db.register(username: 'bob'); final names = (await db.list()).map((e) => e.username).toList(); expect(names, ['alice', 'bob', 'charlie']); }); test('reflects correct hasCredential for each user', () async { await db.register(username: 'probe'); await db.register( username: 'fido', userIdB64: 'dXNlcg==', credentialDataB64: 'Y3JlZA==', ); final list = await db.list(); expect(list.firstWhere((e) => e.username == 'probe').hasCredential, isFalse); expect(list.firstWhere((e) => e.username == 'fido').hasCredential, isTrue); }); }); // ------------------------------------------------------------------------- // Delete // ------------------------------------------------------------------------- group('delete', () { test('removes the user from the list', () async { await db.register(username: 'alice'); await db.delete('alice'); expect(await db.list(), isEmpty); }); test('returns the deleted enrollment', () async { await db.register(username: 'alice', displayName: 'Alice'); final deleted = await db.delete('alice'); expect(deleted.username, 'alice'); expect(deleted.displayName, 'Alice'); }); test('only removes the target user, not others', () async { await db.register(username: 'alice'); await db.register(username: 'bob'); await db.delete('alice'); final names = (await db.list()).map((e) => e.username).toList(); expect(names, ['bob']); }); test('throws StateError when user does not exist', () async { await expectLater( db.delete('nobody'), throwsA(isA()), ); }); }); // ------------------------------------------------------------------------- // Persistence // ------------------------------------------------------------------------- group('persistence', () { test('enrollments survive across new EnrollmentDb instances', () async { await db.register(username: 'alice', displayName: 'Alice'); await db.register(username: 'bob'); final db2 = EnrollmentDb(baseDir: tmp); final names = (await db2.list()).map((e) => e.username).toList(); expect(names, ['alice', 'bob']); }); test('credential data survives reload', () async { await db.register( username: 'alice', userIdB64: 'dXNlcg==', credentialDataB64: 'Y3JlZA==', ); final db2 = EnrollmentDb(baseDir: tmp); final e = await db2.get('alice'); expect(e, isNotNull); expect(e!.hasCredential, isTrue); expect(e.credentialDataB64, 'Y3JlZA=='); }); test('deletion persists across new instances', () async { await db.register(username: 'alice'); await db.delete('alice'); final db2 = EnrollmentDb(baseDir: tmp); expect(await db2.list(), isEmpty); }); test('two concurrent writes both complete without corruption', () async { // Simultaneous register calls must queue, not corrupt the file. await Future.wait([ db.register(username: 'alice'), db.register(username: 'bob'), db.register(username: 'charlie'), ]); final names = (await db.list()).map((e) => e.username).toList(); expect(names, containsAll(['alice', 'bob', 'charlie'])); }); }); // ------------------------------------------------------------------------- // Update // ------------------------------------------------------------------------- group('update', () { test('changes display name', () async { await db.register(username: 'alice', displayName: 'Old Name'); await db.update(username: 'alice', displayName: 'New Name'); final e = await db.get('alice'); expect(e!.displayName, 'New Name'); }); test('updatedAt advances after update', () async { final e1 = await db.register(username: 'alice'); await Future.delayed(const Duration(seconds: 1)); await db.update(username: 'alice', displayName: 'Alice'); final e2 = await db.get('alice'); expect(e2!.updatedAt, greaterThanOrEqualTo(e1.updatedAt)); }); test('throws StateError when user does not exist', () async { await expectLater( db.update(username: 'nobody'), throwsA(isA()), ); }); }); }