import 'package:flutter_test/flutter_test.dart'; import 'package:marianum_mobile/session/account_codec.dart'; import 'package:marianum_mobile/session/session.dart'; import 'package:marianum_mobile/session/session_codec.dart'; void main() { final school = CredentialSession(username: 'max', password: 'pw'); const guardian = GuardianSession(email: 'eltern@x.de'); group('AccountIndex.activate', () { test('adds an unknown account with its own namespace', () { final index = AccountIndex.empty.activate(school, newId: 'a', now: 5); expect(index.activeId, 'a'); final entry = index.active!; expect(entry.kind, SessionKeys.kindCredential); expect(entry.label, 'max'); expect(entry.namespace, 'a'); expect(entry.lastUsed, 5); }); test('legacy account keeps the un-namespaced storage', () { final index = AccountIndex.empty.activate( school, newId: 'a', namespace: '', ); expect(index.active!.namespace, ''); }); test('a known account is reused instead of duplicated', () { final index = AccountIndex.empty .activate(school, newId: 'a') .activate(guardian, newId: 'b') .activate( CredentialSession(username: 'max', password: 'new'), newId: 'c', now: 9, ); expect(index.accounts.map((e) => e.id), ['a', 'b']); expect(index.activeId, 'a'); expect(index.active!.lastUsed, 9); }); test('demo and real account with the same name stay apart', () { final index = AccountIndex.empty .activate(school, newId: 'a') .activate( CredentialSession(username: 'max', password: 'demo', isDemo: true), newId: 'b', ); expect(index.accounts, hasLength(2)); }); }); group('AccountIndex removal', () { final index = AccountIndex.empty .activate(school, newId: 'a', now: 1) .activate(guardian, newId: 'b', now: 3) .select('a', now: 2); test('removing the active account leaves none active', () { final removed = index.remove('a'); expect(removed.activeId, isNull); expect(removed.accounts.map((e) => e.id), ['b']); }); test('removing an inactive account keeps the active one', () { expect(index.remove('b').activeId, 'a'); }); test('takeover picks the most recently used remaining account', () { final third = index.activate( const GuardianSession(email: 'z@x.de'), newId: 'c', now: 1, ); expect(third.remove('a').mostRecentExcept(null)?.id, 'b'); expect(index.mostRecentExcept('b')?.id, 'a'); expect(AccountIndex.empty.mostRecentExcept(null), isNull); }); }); group('AccountIndex codec', () { test('round trip', () { final index = AccountIndex.empty .activate(school, newId: 'a', namespace: '', now: 1) .activate(guardian, newId: 'b', now: 2); final decoded = AccountIndex.decode(index.encode()); expect(decoded.activeId, 'b'); expect(decoded.accounts, hasLength(2)); expect(decoded.byId('a')!.namespace, ''); expect(decoded.byId('b')!.isGuardian, isTrue); expect(decoded.byId('b')!.lastUsed, 2); }); test('display name survives rename and round trip', () { final index = AccountIndex.empty .activate(school, newId: 'a') .activate(guardian, newId: 'b') .rename('b', 'Erika Muster'); final decoded = AccountIndex.decode(index.encode()); expect(decoded.byId('b')!.displayName, 'Erika Muster'); expect(decoded.byId('a')!.displayName, isNull); expect(decoded.activeId, 'b'); }); test('garbage decodes to an empty index', () { expect(AccountIndex.decode(null).accounts, isEmpty); expect(AccountIndex.decode('{nope').accounts, isEmpty); expect(AccountIndex.decode('[]').accounts, isEmpty); }); test('malformed entries are skipped', () { final decoded = AccountIndex.decode( '{"activeId":"a","accounts":[{"id":"a"},' '{"id":"b","kind":"guardian","label":"x"}]}', ); expect(decoded.accounts.map((e) => e.id), ['b']); expect(decoded.active, isNull); }); }); group('vault', () { test('credential session survives the vault with app passwords', () { final session = CredentialSession( username: 'max', password: 'pw', appPassword: 'app', appPasswordTalk: 'talk', usesLoginFlow: true, ); final fields = decodeVault( encodeVault({...sessionVaultFields(session), 'mc_bearer_token': 't'}), ); final restored = decodeSession(fields)! as CredentialSession; expect(restored.username, 'max'); expect(restored.password, 'pw'); expect(restored.nextcloud.appPassword, 'app'); expect(restored.nextcloud.appPasswordTalk, 'talk'); expect(restored.nextcloud.usesLoginFlow, isTrue); expect(fields['mc_bearer_token'], 't'); }); test('guardian demo session survives the vault', () { final fields = decodeVault( encodeVault( sessionVaultFields( const GuardianSession(email: 'e@x.de', isDemo: true), ), ), ); final restored = decodeSession(fields)! as GuardianSession; expect(restored.email, 'e@x.de'); expect(restored.isDemo, isTrue); expect(fields.containsKey(SessionKeys.username), isFalse); }); test('broken vault yields no session', () { expect(decodeSession(decodeVault('garbage')), isNull); }); }); }