k_card/k_phone/test/enrollment_test.dart

241 lines
7.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 332 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>()),
);
});
});
}