feat(train): the Jellyfin token in Train's own store, sealed by the Keystore (ADR 0030, D52)
The token moves from flutter_secure_storage into JellyfinTokens.kt: a file per person in the no-backup folder, sealed with AES-GCM under a key of Android's Keystore, the person's ID as associated data, so that the music job to come can read it as the app does. A token the keyring held is moved over as it is first read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
64366d5420
commit
6bde74f0ba
1 changed files with 342 additions and 18 deletions
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_store.dart';
|
||||
|
||||
/// The Jellyfin token in Train's own store on Android (ADR 0030, D52), over
|
||||
/// the music channel (`JellyfinTokens.kt`, a store in memory here); a token
|
||||
/// the keyring kept before is moved over once and leaves the keyring.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
const channel = MethodChannel('app.halkyon.train/music');
|
||||
late Map<String, String> kept;
|
||||
|
||||
setUp(() {
|
||||
kept = {};
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
final arguments = call.arguments as Map<Object?, Object?>;
|
||||
final person = arguments['person']! as String;
|
||||
switch (call.method) {
|
||||
case 'tokenRead':
|
||||
return kept[person];
|
||||
case 'tokenKeep':
|
||||
if (arguments['token'] case final String token) {
|
||||
kept[person] = token;
|
||||
} else {
|
||||
kept.remove(person);
|
||||
}
|
||||
case 'tokenForgetOthers':
|
||||
kept.removeWhere((key, _) => key != person);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(
|
||||
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null),
|
||||
);
|
||||
|
||||
test(
|
||||
'a token kept is read back from the store, and null forgets it',
|
||||
() async {
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
final tokens = AndroidJellyfinTokenStore('ada');
|
||||
|
||||
await tokens.keep('token-1');
|
||||
expect(await tokens.read(), 'token-1');
|
||||
expect(kept, {'ada': 'token-1'});
|
||||
|
||||
await tokens.keep(null);
|
||||
expect(await tokens.read(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test("the keyring's token is moved over once and leaves the keyring; "
|
||||
"others' are forgotten in both", () async {
|
||||
FlutterSecureStorage.setMockInitialValues({
|
||||
'train_jellyfin:ada': 'before',
|
||||
'train_jellyfin:bob': 'his',
|
||||
'halkyon_account_key': 'not music',
|
||||
});
|
||||
kept['bob'] = 'his';
|
||||
final tokens = AndroidJellyfinTokenStore('ada');
|
||||
|
||||
await tokens.forgetOthers();
|
||||
expect(await tokens.read(), 'before');
|
||||
|
||||
const keyring = FlutterSecureStorage();
|
||||
expect(await keyring.readAll(), {'halkyon_account_key': 'not music'});
|
||||
expect(kept, {'ada': 'before'});
|
||||
});
|
||||
|
||||
test('a token kept anew leaves no old one in the keyring', () async {
|
||||
FlutterSecureStorage.setMockInitialValues({'train_jellyfin:ada': 'before'});
|
||||
final tokens = AndroidJellyfinTokenStore('ada');
|
||||
|
||||
await tokens.keep('after');
|
||||
expect(await tokens.read(), 'after');
|
||||
expect(await const FlutterSecureStorage().readAll(), isEmpty);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue