Skip to content

Make api_request's HTTP client injectable + add request-shape tests#196

Merged
phanan merged 4 commits into
masterfrom
chore/injectable-http-client
May 2, 2026
Merged

Make api_request's HTTP client injectable + add request-shape tests#196
phanan merged 4 commits into
masterfrom
chore/injectable-http-client

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented May 2, 2026

Summary

Closes a long-standing testing gap: lib/utils/api_request.dart called the top-level Http.get / post / put / patch / delete helpers from package:http directly, so nothing in the test suite could observe the URL, method, headers or body the player actually sends. The 422 bug in AlbumProvider.toggleFavorite (where we sent type: 'albums' and the koel server's `FavoriteableType` enum expects type: 'album') slipped through for exactly that reason — there was no place to write a regression test for request shape.

This PR:

  • Adds a module-level `_client` to `api_request.dart`, defaulting to a fresh `http.Client()`, and routes every per-method helper through it.
  • Exposes `setHttpClientForTesting` / `resetHttpClientForTesting` (annotated `@visibleForTesting`) so tests can swap in a `MockClient` from `package:http/testing.dart`.
  • Adds `test/utils/api_request_test.dart` (11 tests) covering URL building, method dispatch for GET/POST/PUT/PATCH/DELETE, header shape (content-type, accept, X-Api-Version, optional bearer), and the 2xx / non-JSON / non-2xx response paths.
  • Adds `test/providers/toggle_favorite_request_shape_test.dart` (4 tests) that pin the singular `type` strings the koel server's `FavoriteableType` enum requires — `album`, `artist`, `podcast`, `radio-station` — for all four providers' `toggleFavorite`.

No production-code behaviour change: the default client is identical to the old direct calls. Every existing caller (`get/post/put/patch/delete` top-level functions) is unchanged.

Test plan

  • `flutter test` — all 354 tests pass (was 339 before; +15 new).
  • `flutter analyze` clean on the changed/new files.

Summary by CodeRabbit

  • Tests
    • Added comprehensive test coverage for API request handling, including HTTP method verification, request formatting, response parsing, and error scenarios.
    • Added test suites for Album, Artist, Playable, Podcast, and Radio Station providers, verifying favorites toggling, updates, subscriptions, caching, and data fetching behaviors.
    • Introduced robust test helpers and utilities to initialize API test environments and inject/mock HTTP interactions for consistent, isolated testing.

Until now lib/utils/api_request.dart called the top-level Http.get /
post / put / patch / delete helpers from package:http directly, so
nothing in the test suite could observe the URL, method, headers or
body the player actually sends. The 422 bug in
AlbumProvider.toggleFavorite (where we sent type: 'albums' and the
koel server expects type: 'album') slipped through for exactly that
reason — there was no place to write a regression test against the
request shape.

This commit:
- Adds a module-level `_client` to api_request.dart, defaulting to a
  fresh http.Client(), and routes every per-method helper through it.
- Exposes setHttpClientForTesting / resetHttpClientForTesting
  (annotated @VisibleForTesting) so tests can swap in a MockClient
  from package:http/testing.dart.
- Adds api_request_test.dart covering URL building, method dispatch
  for GET/POST/PUT/PATCH/DELETE, header shape (content-type, accept,
  X-Api-Version, optional bearer), and 2xx / non-JSON / non-2xx
  response paths.
- Adds toggle_favorite_request_shape_test.dart that pins the singular
  type strings the koel server's FavoriteableType enum requires —
  album, artist, podcast, radio-station — for all four providers.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30a7942b-bee8-45f5-a3f1-90bebab48def

📥 Commits

Reviewing files that changed from the base of the PR and between 45455d4 and 050028b.

📒 Files selected for processing (1)
  • test/utils/api_request_test.dart
✅ Files skipped from review due to trivial changes (1)
  • test/utils/api_request_test.dart

📝 Walkthrough

Walkthrough

Replaces direct static HTTP calls with a module-level, overridable Http.Client and test helpers; adds test-only client injection/reset functions, test environment/setup utilities, a capturing mock HTTP client, and extensive provider & API request tests exercising request composition, headers, responses, optimistic updates, and rollback behavior.

Changes

HTTP client injection + test harness and provider tests

Layer / File(s) Summary
Data Shape / Test scaffolding
test/helpers/api_test_setup.dart
Adds test harness: initApiTestEnvironment() for widget bindings + GetStorage temp directory, setUpApiTest() / tearDownApiTest() for per-test preference state, CapturingClient (records requests, configurable responses), and CapturedRequest with jsonBody getter.
HTTP client wiring
lib/utils/api_request.dart
Introduces module-level Http.Client _client, @visibleForTesting functions setHttpClientForTesting(Http.Client) and resetHttpClientForTesting(), and updates internal request dispatch to call _client.get/post/patch/put/delete(...) instead of static Http.* methods; request API and response handling logic unchanged.
API request tests
test/utils/api_request_test.dart
Adds tests validating URL construction, method dispatch for GET/POST/PUT/PATCH/DELETE, JSON body encoding when data provided, required headers (content-type, accept, x-api-version, Authorization presence/absence based on preferences.apiToken), and response handling (2xx JSON -> decoded map, 2xx non-JSON -> null, non-2xx -> HttpResponseException).
Provider behavior tests
test/providers/*_provider_test.dart (album, artist, playable, podcast, radio_station)
Adds comprehensive provider tests covering optimistic toggleFavorite (POST /api/favorites/toggle), create/update/add/remove/fetch behaviors (correct method/URL/body, merging/parsing responses), caching/forceRefresh semantics for podcast episodes, listener notification counts, and error rollback paths that rethrow HttpResponseException.

Sequence Diagram(s)

sequenceDiagram
  participant Provider
  participant ApiLayer as api_request.dart
  participant HttpClient as Http.Client (in tests: CapturingClient)
  participant Server

  Provider->>ApiLayer: call api.get/post/put/patch/delete(path, data?)
  ApiLayer->>HttpClient: _client.<verb>(uri, headers, body?)
  HttpClient->>Server: HTTP request (captures method, url, headers, body)
  Server-->>HttpClient: HTTP response (status, body)
  HttpClient-->>ApiLayer: Response
  ApiLayer-->>Provider: returns decoded JSON / null or throws HttpResponseException
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped into code with a twitching nose,

Swapped static calls for a client that grows,
Captured each request, recorded each heady beat,
Providers dance optimistically, rollback if they meet defeat,
Tests hum like carrots — crunchy, precise, and sweet. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: making api_request's HTTP client injectable and adding comprehensive request-shape tests to enable observing outgoing HTTP calls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/injectable-http-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 10 minutes and 3 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Asserting that toggleFavorite POSTs type:album doesn't catch anything
the source doesn't already say plainly. If someone typos the source
they'll typo the test the same way — which is exactly how the
original 422 bug would have been written and would have passed.

Keep the actually-useful piece: the injectable http.Client and the
api_request_test.dart that exercises real request/response behaviour.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/utils/api_request_test.dart`:
- Around line 170-181: The test "throws HttpResponseException on non-2xx" is
currently passing a Future-returning closure to expect without awaiting it;
change the assertion to await the asynchronous expectation (e.g., use await
expectLater or await expect with the Future) so the test actually awaits
api.post and fails if no exception is thrown. Locate the test that sets up
_captureClient, calls api.setHttpClientForTesting(mock.client) and invokes
api.post('favorites/toggle', ...), and replace the synchronous expect(() =>
api.post(...), throwsA(isA<HttpResponseException>())) with an awaited async
expectation that directly awaits the Future returned by api.post and asserts
throwsA(isA<HttpResponseException>)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbd4e717-1683-471e-a109-77f90f9ec846

📥 Commits

Reviewing files that changed from the base of the PR and between 97f7c0c and 49eaedd.

📒 Files selected for processing (3)
  • lib/utils/api_request.dart
  • test/providers/toggle_favorite_request_shape_test.dart
  • test/utils/api_request_test.dart

Comment thread test/utils/api_request_test.dart
Behaviour tests for AlbumProvider, ArtistProvider, RadioStationProvider,
PodcastProvider, and PlayableProvider.fetchForPodcast — driven by a
shared MockClient harness so each test exercises the actual
request → response → state-mutation → notifyListeners pipeline.

For each operation the suite locks down:
  - the URL, method, and JSON body the provider sends;
  - the model fields the response merges into;
  - the listener notification count;
  - the rollback + rethrow shape on a non-2xx response, where the
    provider does an optimistic mutation (toggleFavorite,
    unsubscribePodcast).

The CapturingClient helper in test/helpers/api_test_setup.dart wires
in the path_provider mock + GetStorage init so every provider test
gets a clean preferences slate (per-isolate temp dir to avoid the
'./Preferences.gs' file lock contention when test files run in
parallel).

Net 25 new tests; 375 pass total.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
test/utils/api_request_test.dart (1)

129-137: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Await the async throw assertion in the non-2xx test

This still uses a non-awaited async expectation pattern; use await expectLater(...) to ensure the test actually fails on regression.

Proposed fix
-      expect(
-        () => api.post('favorites/toggle', data: {}),
-        throwsA(isA<HttpResponseException>()),
-      );
+      await expectLater(
+        api.post('favorites/toggle', data: {}),
+        throwsA(isA<HttpResponseException>()),
+      );
#!/bin/bash
# Verify the assertion style used in the non-2xx test.
rg -n -C3 "throws HttpResponseException on non-2xx|expect\\(|expectLater\\(" test/utils/api_request_test.dart
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/utils/api_request_test.dart` around lines 129 - 137, The test "throws
HttpResponseException on non-2xx" currently uses a non-awaited expect with a
closure; change it to await the async assertion by replacing the expect(() =>
api.post('favorites/toggle', data: {}), throwsA(isA<HttpResponseException>()))
with await expectLater(api.post('favorites/toggle', data: {}),
throwsA(isA<HttpResponseException>())). This uses the existing CapturingClient
and HttpResponseException symbols and ensures the Future returned by api.post is
awaited by the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/utils/api_request_test.dart`:
- Around line 129-137: The test "throws HttpResponseException on non-2xx"
currently uses a non-awaited expect with a closure; change it to await the async
assertion by replacing the expect(() => api.post('favorites/toggle', data: {}),
throwsA(isA<HttpResponseException>())) with await
expectLater(api.post('favorites/toggle', data: {}),
throwsA(isA<HttpResponseException>())). This uses the existing CapturingClient
and HttpResponseException symbols and ensures the Future returned by api.post is
awaited by the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d91896b7-a1aa-4d7d-a1e8-52d2bcf6a046

📥 Commits

Reviewing files that changed from the base of the PR and between 49eaedd and 45455d4.

📒 Files selected for processing (7)
  • test/helpers/api_test_setup.dart
  • test/providers/album_provider_test.dart
  • test/providers/artist_provider_test.dart
  • test/providers/playable_provider_test.dart
  • test/providers/podcast_provider_test.dart
  • test/providers/radio_station_provider_test.dart
  • test/utils/api_request_test.dart

expect(closure, throwsA(...)) on a Future-returning closure resolves
asynchronously. Without awaiting, the test body returns before the
assertion completes — so a regression where the throw stops happening
would pass silently. Switch to await expectLater so the test actually
waits for the throw.
@phanan phanan merged commit 153af44 into master May 2, 2026
2 of 3 checks passed
@phanan phanan deleted the chore/injectable-http-client branch May 2, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant