Files
plezy/test/services/trackers/anilist_tracker_test.dart
T
edde746 78090d8bd6 refactor(trackers): unify session and account store across services
Replace the per-tracker json_serializable session classes and account stores with a shared TrackerSession (service-aware persisted validation), a single TrackerAccountStore, a shared TrackerHttpClient, and an AnimeListTrackerBase mixin for MAL/AniList. Scope the per-service rebind guard so a disconnect can't abort a racing profile load. Add migration-safety, MAL refresh, episode-count cache, and content-type tests.
2026-06-27 10:05:03 +02:00

260 lines
8.1 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/testing.dart';
import 'package:http/http.dart' as http;
import 'package:plezy/models/trackers/anime_ids.dart';
import 'package:plezy/models/trackers/tracker_context.dart';
import 'package:plezy/services/trackers/anilist/anilist_tracker.dart';
import 'package:plezy/services/trackers/tracker_session.dart';
import 'package:plezy/utils/external_ids.dart';
TrackerSession _session() {
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
return TrackerSession(accessToken: 'token', expiresAt: now + 86400, createdAt: now);
}
TrackerContext _episode({int anilistId = 21, int episodeNumber = 12, int? animeProgress = 12}) {
return TrackerContext.episode(
external: const ExternalIds(tvdb: 1),
anime: AnimeIds(anilist: anilistId),
ratingKey: 'episode-1',
libraryGlobalKey: null,
season: 1,
episodeNumber: episodeNumber,
animeProgress: animeProgress,
);
}
void main() {
group('AnilistTracker', () {
final tracker = AnilistTracker.instance;
tearDown(() {
tracker.rebindSession(null, onSessionInvalidated: () {});
});
test('marks completed when scoped progress reaches AniList total', () async {
final saved = <Map<String, dynamic>>[];
final client = MockClient((request) async {
final body = json.decode(request.body) as Map<String, dynamic>;
final query = body['query'] as String;
if (query.contains('Media(id:')) {
return http.Response(
json.encode({
'data': {
'Media': {'episodes': 12},
},
}),
200,
);
}
if (query.contains('SaveMediaListEntry')) {
saved.add((body['variables'] as Map).cast<String, dynamic>());
return http.Response(
json.encode({
'data': {
'SaveMediaListEntry': {'id': 1},
},
}),
200,
);
}
fail('Unexpected AniList query: $query');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.markWatched(_episode(animeProgress: 13));
expect(saved.single, {'mediaId': 21, 'progress': 12, 'status': 'COMPLETED'});
});
test('keeps fallback local progress as current without total lookup', () async {
final saved = <Map<String, dynamic>>[];
final client = MockClient((request) async {
final body = json.decode(request.body) as Map<String, dynamic>;
final query = body['query'] as String;
if (query.contains('SaveMediaListEntry')) {
saved.add((body['variables'] as Map).cast<String, dynamic>());
return http.Response(
json.encode({
'data': {
'SaveMediaListEntry': {'id': 1},
},
}),
200,
);
}
fail('Unexpected AniList query: $query');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.markWatched(_episode(animeProgress: null));
expect(saved.single, {'mediaId': 21, 'progress': 12, 'status': 'CURRENT'});
});
test('keeps progress current when AniList total is unknown', () async {
final saved = <Map<String, dynamic>>[];
final client = MockClient((request) async {
final body = json.decode(request.body) as Map<String, dynamic>;
final query = body['query'] as String;
if (query.contains('Media(id:')) {
return http.Response(
json.encode({
'data': {
'Media': {'episodes': null},
},
}),
200,
);
}
if (query.contains('SaveMediaListEntry')) {
saved.add((body['variables'] as Map).cast<String, dynamic>());
return http.Response(
json.encode({
'data': {
'SaveMediaListEntry': {'id': 1},
},
}),
200,
);
}
fail('Unexpected AniList query: $query');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.markWatched(_episode());
expect(saved.single, {'mediaId': 21, 'progress': 12, 'status': 'CURRENT'});
});
test('episode unwatch is a no-op', () async {
final requests = <http.Request>[];
final client = MockClient((request) async {
requests.add(request);
fail('Unexpected ${request.method} ${request.url}');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.markUnwatched(_episode(animeProgress: 1));
expect(requests, isEmpty);
});
test('removeFromList removes anime entry', () async {
final variables = <Map<String, dynamic>>[];
final client = MockClient((request) async {
final body = json.decode(request.body) as Map<String, dynamic>;
final query = body['query'] as String;
variables.add((body['variables'] as Map).cast<String, dynamic>());
if (query.contains('mediaListEntry')) {
return http.Response(
json.encode({
'data': {
'Media': {
'mediaListEntry': {'id': 99},
},
},
}),
200,
);
}
if (query.contains('DeleteMediaListEntry')) {
return http.Response(
json.encode({
'data': {
'DeleteMediaListEntry': {'deleted': true},
},
}),
200,
);
}
fail('Unexpected AniList query: $query');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.removeFromList(_episode());
expect(variables, [
{'mediaId': 21},
{'id': 99},
]);
});
test('caches the episode count across repeated markWatched calls', () async {
var counts = 0;
final client = MockClient((request) async {
final body = json.decode(request.body) as Map<String, dynamic>;
final query = body['query'] as String;
if (query.contains('Media(id:')) {
counts++;
return http.Response(
json.encode({
'data': {
'Media': {'episodes': 12},
},
}),
200,
);
}
if (query.contains('SaveMediaListEntry')) {
return http.Response(
json.encode({
'data': {
'SaveMediaListEntry': {'id': 1},
},
}),
200,
);
}
fail('Unexpected AniList query: $query');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.markWatched(_episode());
await tracker.markWatched(_episode());
expect(counts, 1);
});
test('re-fetches the episode count after a failed lookup', () async {
var counts = 0;
final client = MockClient((request) async {
final body = json.decode(request.body) as Map<String, dynamic>;
final query = body['query'] as String;
if (query.contains('Media(id:')) {
counts++;
return counts == 1
? http.Response('boom', 500)
: http.Response(
json.encode({
'data': {
'Media': {'episodes': 12},
},
}),
200,
);
}
if (query.contains('SaveMediaListEntry')) {
return http.Response(
json.encode({
'data': {
'SaveMediaListEntry': {'id': 1},
},
}),
200,
);
}
fail('Unexpected AniList query: $query');
});
tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client);
await tracker.markWatched(_episode());
await tracker.markWatched(_episode());
expect(counts, 2);
});
});
}