summaryrefslogtreecommitdiff
path: root/player-android/test/api
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-21 23:01:34 +0300
committerPaul Buetow <paul@buetow.org>2026-05-21 23:01:34 +0300
commit5ca68ad6430b968039003926aefa98f268f129b0 (patch)
treef93aab26ea8dfdb42e1d64e9a764b11162fcf539 /player-android/test/api
parent444fb60ccacf7cba02467f7d6af2efd5d44e3b3b (diff)
Implement all remaining PlayerApiClient stubs and add unit tests
Implements every previously-unimplemented method in DioPlayerApiClient: shares (listSharesForMedia, listMyShares, revokeShare), public shared-media endpoints (getSharedMediaPage, streamSharedMedia, getSharedThumbnail, downloadSharedMedia), config (getConfig), sets (getSetCover, updateSetCover, uploadToSet), media helpers (regenerateThumbnail, deleteMedia, restoreMedia), progress batch (batchUpdateProgress), podcasts (listPodcasts, listEpisodes, downloadEpisode, toggleEpisodeComplete), admin users/permissions/scanner (listUsers, createUser, deleteUser, listPermissions, grantPermission, revokePermission, triggerRescan, getScanProgress, listTrash), and API tokens (listAPITokens, createAPIToken, revokeAPIToken). Adds listAPITokens/createAPIToken/revokeAPIToken/batchUpdateProgress to the abstract PlayerApiClient. Corrects listPermissions return type from List<Map> to Map<String,dynamic> to match the server's single-object response. Adds 18 new unit tests covering shares (listMyShares success/empty/401, revokeShare success/404/401) and podcasts (listEpisodes success/pagination/ empty/401, toggleEpisodeComplete success/404/401). All 264 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'player-android/test/api')
-rw-r--r--player-android/test/api/dio_player_api_client_test.dart244
1 files changed, 244 insertions, 0 deletions
diff --git a/player-android/test/api/dio_player_api_client_test.dart b/player-android/test/api/dio_player_api_client_test.dart
index 401ba41..f4ae81b 100644
--- a/player-android/test/api/dio_player_api_client_test.dart
+++ b/player-android/test/api/dio_player_api_client_test.dart
@@ -477,4 +477,248 @@ void main() {
expect(() => client.getThumbnail(99), throwsA(isA<DioException>()));
});
});
+
+ // ---------------------------------------------------------------------------
+ // listMyShares
+ // ---------------------------------------------------------------------------
+
+ group('listMyShares', () {
+ /// Minimal valid Share JSON matching the GET /api/v1/shares response schema.
+ Map<String, dynamic> shareJson({
+ String token = 'abc123xyz',
+ int mediaId = 42,
+ }) =>
+ {
+ 'token': token,
+ 'media_id': mediaId,
+ 'created_by': 3,
+ 'created_at': '2026-05-17T10:00:00.000Z',
+ 'expires_at': '2026-05-24T10:00:00.000Z',
+ 'max_uses': null,
+ 'used_count': 2,
+ };
+
+ test('success — returns list of Share objects', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/shares',
+ (server) => server.reply(200, [
+ shareJson(),
+ shareJson(token: 'xyz789abc', mediaId: 7),
+ ]),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ final shares = await client.listMyShares();
+
+ expect(shares, hasLength(2));
+ expect(shares[0].token, 'abc123xyz');
+ expect(shares[0].mediaId, 42);
+ expect(shares[0].usedCount, 2);
+ expect(shares[1].token, 'xyz789abc');
+ expect(shares[1].mediaId, 7);
+ });
+
+ test('success — empty list when user has no shares', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/shares',
+ (server) => server.reply(200, <dynamic>[]),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ final shares = await client.listMyShares();
+
+ expect(shares, isEmpty);
+ });
+
+ test('401 — DioException is propagated', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/shares',
+ (server) => server.reply(401, {'error': 'unauthorized'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(() => client.listMyShares(), throwsA(isA<DioException>()));
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // revokeShare
+ // ---------------------------------------------------------------------------
+
+ group('revokeShare', () {
+ test('success — 200 completes without error', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onDelete(
+ '/api/v1/shares/abc123xyz',
+ (server) => server.reply(200, {'status': 'ok'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ await expectLater(client.revokeShare('abc123xyz'), completes);
+ });
+
+ test('404 — DioException when share not found', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onDelete(
+ '/api/v1/shares/missing_token',
+ (server) => server.reply(404, {'error': 'not found'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(
+ () => client.revokeShare('missing_token'),
+ throwsA(isA<DioException>()),
+ );
+ });
+
+ test('401 — DioException when unauthenticated', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onDelete(
+ '/api/v1/shares/abc123xyz',
+ (server) => server.reply(401, {'error': 'unauthorized'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(
+ () => client.revokeShare('abc123xyz'),
+ throwsA(isA<DioException>()),
+ );
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // listEpisodes
+ // ---------------------------------------------------------------------------
+
+ group('listEpisodes', () {
+ /// Minimal valid PodcastEpisode JSON matching the API schema.
+ Map<String, dynamic> episodeJson({
+ int id = 10,
+ String title = 'Episode 1: Introduction',
+ }) =>
+ {
+ 'id': id,
+ 'feed_id': 1,
+ 'media_id': null,
+ 'guid': 'episode-guid-$id',
+ 'title': title,
+ 'description': 'A great episode.',
+ 'published_at': '2026-01-05T00:00:00.000Z',
+ 'episode_url': 'https://example.com/ep$id.mp3',
+ 'duration_seconds': 3600.0,
+ 'file_size': 52428800,
+ 'file_name': 'ep$id.mp3',
+ 'is_downloaded': false,
+ 'is_completed': false,
+ 'position_seconds': 0.0,
+ 'created_at': '2026-01-05T01:00:00.000Z',
+ };
+
+ test('success — returns list of PodcastEpisode objects', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/podcasts/4/episodes',
+ (server) => server.reply(200, [
+ episodeJson(),
+ episodeJson(id: 11, title: 'Episode 2: Deep Dive'),
+ ]),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ final episodes = await client.listEpisodes(4);
+
+ expect(episodes, hasLength(2));
+ expect(episodes[0].id, 10);
+ expect(episodes[0].title, 'Episode 1: Introduction');
+ expect(episodes[0].isDownloaded, isFalse);
+ expect(episodes[0].isCompleted, isFalse);
+ expect(episodes[1].id, 11);
+ expect(episodes[1].title, 'Episode 2: Deep Dive');
+ });
+
+ test('success with pagination — query params are forwarded', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/podcasts/4/episodes',
+ (server) => server.reply(200, [episodeJson()]),
+ queryParameters: {'limit': 20, 'offset': 40},
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ final episodes = await client.listEpisodes(4, limit: 20, offset: 40);
+
+ expect(episodes, hasLength(1));
+ expect(episodes[0].id, 10);
+ });
+
+ test('empty — returns empty list', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/podcasts/4/episodes',
+ (server) => server.reply(200, <dynamic>[]),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(await client.listEpisodes(4), isEmpty);
+ });
+
+ test('401 — DioException is propagated', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onGet(
+ '/api/v1/podcasts/4/episodes',
+ (server) => server.reply(401, {'error': 'unauthorized'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(() => client.listEpisodes(4), throwsA(isA<DioException>()));
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // toggleEpisodeComplete
+ // ---------------------------------------------------------------------------
+
+ group('toggleEpisodeComplete', () {
+ test('success — 204 completes without error', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onPost(
+ '/api/v1/podcasts/episodes/10/complete',
+ (server) => server.reply(204, null),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ await expectLater(client.toggleEpisodeComplete(10), completes);
+ });
+
+ test('404 — DioException when episode not found', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onPost(
+ '/api/v1/podcasts/episodes/999/complete',
+ (server) => server.reply(404, {'error': 'not found'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(
+ () => client.toggleEpisodeComplete(999),
+ throwsA(isA<DioException>()),
+ );
+ });
+
+ test('401 — DioException when unauthenticated', () async {
+ final (:dio, :adapter) = _buildTestDio();
+ adapter.onPost(
+ '/api/v1/podcasts/episodes/10/complete',
+ (server) => server.reply(401, {'error': 'unauthorized'}),
+ );
+
+ final client = DioPlayerApiClient(dio: dio);
+ expect(
+ () => client.toggleEpisodeComplete(10),
+ throwsA(isA<DioException>()),
+ );
+ });
+ });
}