fix(runtime): harden application service boundaries

This commit is contained in:
edde746
2026-07-24 03:46:46 +02:00
parent 658da37b48
commit e0bf66eea8
309 changed files with 32574 additions and 4369 deletions
@@ -0,0 +1,188 @@
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/exceptions/media_server_exceptions.dart';
import 'package:plezy/media/ids.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/services/api_cache.dart';
import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/utils/media_server_http_client.dart';
class _CacheClient with MediaServerCacheMixin implements MediaServerClient {
_CacheClient(this.cache);
@override
final ApiCache cache;
@override
ServerId get serverId => ServerId('cache-server');
@override
MediaBackend get backend => MediaBackend.plex;
bool offline = false;
@override
bool get isOfflineMode => offline;
@override
void setOfflineMode(bool value) => offline = value;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
late AppDatabase database;
late PlexApiCache cache;
late _CacheClient client;
setUp(() {
database = AppDatabase.forTesting(NativeDatabase.memory());
PlexApiCache.initialize(database);
cache = PlexApiCache.instance;
client = _CacheClient(cache);
});
tearDown(() async {
await database.close();
});
test('cache-first rejects decoded 401 and 500 before parsing or caching', () async {
for (final status in [401, 500]) {
var networkCalls = 0;
var parserCalls = 0;
final key = '/metadata/status-$status';
await expectLater(
client.fetchWithCacheFirst<String>(
cacheScope: client.serverId,
cacheKey: key,
networkCall: () async {
networkCalls++;
return MediaServerResponse(
statusCode: status,
data: {
'MediaContainer': {
'Metadata': [
{'ratingKey': 'must-not-parse'},
],
},
},
headers: const {},
);
},
parseCache: (_) => 'cached',
parseResponse: (_) {
parserCalls++;
return 'parsed';
},
),
throwsA(isA<MediaServerHttpException>().having((error) => error.statusCode, 'statusCode', status)),
);
expect(networkCalls, 1);
expect(parserCalls, 0);
expect(await cache.get(client.serverId, key), isNull);
}
});
test('cache-first validates status even when response caching is disabled', () async {
var parserCalls = 0;
await expectLater(
client.fetchWithCacheFirst<String>(
cacheScope: client.serverId,
cacheKey: '/metadata/no-cache',
cacheResponse: false,
networkCall: () async =>
MediaServerResponse(statusCode: 500, data: const {'mustNotParse': true}, headers: const {}),
parseCache: (_) => 'cached',
parseResponse: (_) {
parserCalls++;
return 'parsed';
},
),
throwsA(isA<MediaServerHttpException>().having((error) => error.statusCode, 'statusCode', 500)),
);
expect(parserCalls, 0);
expect(await cache.get(client.serverId, '/metadata/no-cache'), isNull);
});
test('successful miss is parsed and cached exactly once', () async {
var networkCalls = 0;
var parserCalls = 0;
const body = {
'MediaContainer': {
'Metadata': [
{'ratingKey': '42'},
],
},
};
final value = await client.fetchWithCacheFirst<String>(
cacheScope: client.serverId,
cacheKey: '/metadata/success',
networkCall: () async {
networkCalls++;
return MediaServerResponse(statusCode: 200, data: body, headers: const {});
},
parseCache: (_) => 'cached',
parseResponse: (response) {
parserCalls++;
return (response.data as Map<String, dynamic>)['MediaContainer'].toString();
},
);
expect(value, contains('ratingKey'));
expect(networkCalls, 1);
expect(parserCalls, 1);
expect(await cache.get(client.serverId, '/metadata/success'), body);
});
test('prepopulated cache wins without network or response parsing', () async {
const body = {'cached': true};
await cache.put(client.serverId, '/metadata/cached', body);
var responseParserCalls = 0;
final value = await client.fetchWithCacheFirst<String>(
cacheScope: client.serverId,
cacheKey: '/metadata/cached',
networkCall: () => fail('Network must not be called for a cache hit'),
parseCache: (cached) => (cached as Map<String, dynamic>)['cached'].toString(),
parseResponse: (_) {
responseParserCalls++;
return 'network';
},
);
expect(value, 'true');
expect(responseParserCalls, 0);
});
test('offline cache miss returns null without network or parsers', () async {
client.setOfflineMode(true);
var cacheParserCalls = 0;
var responseParserCalls = 0;
final value = await client.fetchWithCacheFirst<String>(
cacheScope: client.serverId,
cacheKey: '/metadata/offline-miss',
networkCall: () => fail('Network must not be called while offline'),
parseCache: (_) {
cacheParserCalls++;
return 'cached';
},
parseResponse: (_) {
responseParserCalls++;
return 'network';
},
);
expect(value, isNull);
expect(cacheParserCalls, 0);
expect(responseParserCalls, 0);
});
}
+71
View File
@@ -0,0 +1,71 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_version.dart';
void main() {
MediaVersion version(String resolution, String codec, String container, {String id = ''}) {
return MediaVersion(id: id, videoResolution: resolution, videoCodec: codec, container: container);
}
group('MediaVersion.findMatchingIndex', () {
test('exact match globally outranks an earlier resolution+codec match', () {
final versions = [version('1080', 'h264', 'mp4'), version('4k', 'hevc', 'mkv')];
expect(MediaVersion.findMatchingIndex(versions, {'1080:h264:mkv', '4k:hevc:mkv'}), 1);
});
test('exact match globally outranks an earlier resolution-only match', () {
final versions = [version('1080', 'vp9', 'mp4'), version('4k', 'hevc', 'mkv')];
expect(MediaVersion.findMatchingIndex(versions, {'1080:h264:avi', '4k:hevc:mkv'}), 1);
});
test('resolution+codec globally outranks an earlier resolution-only match', () {
final versions = [version('1080', 'vp9', 'mp4'), version('4k', 'hevc', 'mp4')];
expect(MediaVersion.findMatchingIndex(versions, {'1080:h264:avi', '4k:hevc:mkv'}), 1);
});
test('skips malformed signatures without blocking a later valid exact match', () {
final versions = [version('1080', 'h264', 'mkv')];
expect(MediaVersion.findMatchingIndex(versions, {'malformed', '1080:h264:mkv'}), 0);
expect(MediaVersion.findMatchingIndex(versions, {'malformed', 'also:malformed'}), isNull);
});
test('earlier accepted signature wins a same-tier tie even with a later candidate', () {
final versions = [version('4k', 'hevc', 'mp4'), version('1080', 'h264', 'mp4')];
expect(MediaVersion.findMatchingIndex(versions, {'1080:h264:mkv', '4k:hevc:mkv'}), 1);
});
test('retains valid three-field signatures with empty fields', () {
const versions = [MediaVersion(id: 'empty')];
expect(MediaVersion.findMatchingIndex(versions, {'::'}), 0);
});
test('earlier candidate wins when one signature has multiple same-tier matches', () {
final versions = [version('1080', 'h264', 'mp4'), version('1080', 'h264', 'avi')];
expect(MediaVersion.findMatchingIndex(versions, {'1080:h264:mkv'}), 0);
});
test('singleton signatures retain exact then codec then resolution priority', () {
final resolutionOnly = version('1080', 'vp9', 'mp4');
final resolutionAndCodec = version('1080', 'h264', 'mp4');
final exact = version('1080', 'h264', 'mkv');
const accepted = {'1080:h264:mkv'};
expect(MediaVersion.findMatchingIndex([resolutionOnly, resolutionAndCodec, exact], accepted), 2);
expect(MediaVersion.findMatchingIndex([resolutionOnly, resolutionAndCodec], accepted), 1);
expect(MediaVersion.findMatchingIndex([resolutionOnly], accepted), 0);
});
test('returns null for empty candidates or accepted signatures', () {
final candidate = version('1080', 'h264', 'mkv');
expect(MediaVersion.findMatchingIndex(const [], {'1080:h264:mkv'}), isNull);
expect(MediaVersion.findMatchingIndex([candidate], const {}), isNull);
});
});
}