241 lines
7.6 KiB
Dart
241 lines
7.6 KiB
Dart
// 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<StateError>()),
|
||
);
|
||
});
|
||
|
||
test('duplicate check is case-insensitive', () async {
|
||
await db.register(username: 'alice');
|
||
await expectLater(
|
||
db.register(username: 'ALICE'),
|
||
throwsA(isA<StateError>()),
|
||
);
|
||
});
|
||
|
||
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<ArgumentError>()),
|
||
);
|
||
});
|
||
|
||
test('rejects username with special characters', () async {
|
||
await expectLater(
|
||
db.register(username: 'alice!'),
|
||
throwsA(isA<ArgumentError>()),
|
||
);
|
||
});
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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<StateError>()),
|
||
);
|
||
});
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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<StateError>()),
|
||
);
|
||
});
|
||
});
|
||
}
|