test: Update Tests so they can run in LIVE, CAPTURE or MOCK modes#519
test: Update Tests so they can run in LIVE, CAPTURE or MOCK modes#519kingargyle merged 1 commit intodevelopfrom
Conversation
WalkthroughAdds test infrastructure and fixtures: MockWebServer support, an interceptor-injectable EmbyAPIClient, response-capture interceptor, several mock JSON resources, build config/test constants, and removes the paging_support_plan.md document. Changes
Sequence Diagram(s)sequenceDiagram
participant Test
participant EmbyClient as EmbyAPIClient
participant Interceptors as OkHttp Interceptors
participant Server as MockWebServer
participant FS as FileSystem
Test->>Server: enqueue mock response (or start real server)
Test->>EmbyClient: construct(clientConfig with additionalInterceptors)
Test->>EmbyClient: perform API request
EmbyClient->>Interceptors: execute chain (includes capture interceptor)
Interceptors->>Server: send HTTP request
Server-->>Interceptors: return HTTP response
Interceptors->>FS: (capture interceptor) write response body to file if successful
Interceptors-->>EmbyClient: return response
EmbyClient-->>Test: deliver parsed result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClientTest.kt`:
- Around line 36-37: The test currently starts MockWebServer on a hardcoded port
with mockWebServer.start(8096), which can cause port conflicts; change it to use
mockWebServer.start() so the OS assigns an available port and rely on
mockWebServer.url("/") as the base URL for requests; update any references
expecting port 8096 to use mockWebServer.url("/") or mockWebServer.port() if
needed and ensure MockWebServer is still started and shutdown in the
EmbyAPIClientTest setup/teardown.
In `@emby-lib/src/test/resources/mock-data/emby_Users_AuthenticateByName.json`:
- Around line 88-107: SessionInfo.UserName in the fixture does not match
User.Name — update the JSON so "SessionInfo.UserName" equals "test" (the value
of "User.Name") to keep the mock consistent (look for the SessionInfo.UserName
and User.Name fields in this file); also replace the literal AccessToken with a
clearly synthetic placeholder (e.g. "mock-access-token") to avoid accidental
secrets scanning confusion while keeping the test data deterministic.
🧹 Nitpick comments (4)
emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyResponseCaptureInterceptor.kt (1)
14-21: Filename collisions: query parameters are not included in the captured filename.Different requests to the same path but with different query parameters (e.g.,
/Items?parentId=3vs/Items?parentId=5) will overwrite each other since onlyencodedPathis used. If you intend to capture distinct responses per query, consider incorporating query parameters into the filename.emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClientTest.kt (1)
201-214: Dead code:authenticate()helper is unused.The private
authenticate()method (lines 201–214) is not called by any test. It also relies on finding a user without a password, which doesn't exist in the mock data (both users haveHasPassword: true). Consider removing it.emby-lib/build.gradle.kts (2)
43-54: Consider reading test credentials from environment variables orgradle.properties.Hardcoding
testMode,serverUrl,testUser, andtestPasswordinbuild.gradle.ktsrequires editing the build file to switch between MOCK/LIVE/CAPTURE modes. UsingfindProperty()orSystem.getenv()with defaults would make mode switching easier and avoid accidentally committing real credentials.Proposed approach
- val testMode = "MOCK" - val serverUrl = "http://localhost:8096/" - val testUser = "test" - val testPassword = "unknown" + val testMode = findProperty("test.mode")?.toString() ?: "MOCK" + val serverUrl = findProperty("test.serverUrl")?.toString() ?: "http://localhost:8096/" + val testUser = findProperty("test.user")?.toString() ?: "test" + val testPassword = findProperty("test.password")?.toString() ?: "unknown"
57-63: Release build type carries test-only configuration.
TEST_MODE,SERVER_URL,TEST_USER, andTEST_PASSWORDare test infrastructure fields that have no purpose in the release build type. While harmless, they add noise to the releaseBuildConfig.
| mockWebServer = MockWebServer() | ||
| mockWebServer.start(8096) |
There was a problem hiding this comment.
Hardcoded port 8096 may cause port conflicts.
MockWebServer().start(8096) will fail if the port is already in use (e.g., a real Emby server, parallel test runs). In MOCK mode, the server just needs any available port. Consider using mockWebServer.start() (random port) since mockWebServer.url("/") already returns the correct base URL.
Proposed fix
mockWebServer = MockWebServer()
- mockWebServer.start(8096)
+ mockWebServer.start()🤖 Prompt for AI Agents
In
`@emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClientTest.kt`
around lines 36 - 37, The test currently starts MockWebServer on a hardcoded
port with mockWebServer.start(8096), which can cause port conflicts; change it
to use mockWebServer.start() so the OS assigns an available port and rely on
mockWebServer.url("/") as the base URL for requests; update any references
expecting port 8096 to use mockWebServer.url("/") or mockWebServer.port() if
needed and ensure MockWebServer is still started and shutdown in the
EmbyAPIClientTest setup/teardown.
| "RemoteEndPoint": "1.1.1.1", | ||
| "Protocol": "HTTP/1.1", | ||
| "PlayableMediaTypes": [], | ||
| "PlaylistIndex": 0, | ||
| "PlaylistLength": 0, | ||
| "Id": "a78435917c20f9b961f71546f1d3e969", | ||
| "ServerId": "9db55b26aba245c1ad621fce931b3058", | ||
| "UserId": "a99be67f778f4ebd82d22ae04153520e", | ||
| "UserName": "kingargyle", | ||
| "Client": "Android", | ||
| "LastActivityDate": "2026-02-16T03:03:10.0942894Z", | ||
| "DeviceName": "unknown robolectric ", | ||
| "InternalDeviceId": 3, | ||
| "DeviceId": "ffffffff-e76f-720e-ffff-ffffef05ac4a", | ||
| "ApplicationVersion": "3.0-M1.0", | ||
| "SupportedCommands": [], | ||
| "SupportsRemoteControl": false | ||
| }, | ||
| "AccessToken": "b51b2146782d47589e2f1c1473ba5912", | ||
| "ServerId": "9db55b26aba245c1ad621fce931b3058" |
There was a problem hiding this comment.
Minor inconsistency: SessionInfo.UserName doesn't match User.Name.
User.Name is "test" (line 3) but SessionInfo.UserName is "kingargyle" (line 96). This was likely captured from a real session. While it won't break current tests, it could cause confusion or failures if tests later assert on session username consistency.
The Gitleaks warning on line 106 (AccessToken) is a false positive — this is mock test fixture data.
🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 106-106: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In `@emby-lib/src/test/resources/mock-data/emby_Users_AuthenticateByName.json`
around lines 88 - 107, SessionInfo.UserName in the fixture does not match
User.Name — update the JSON so "SessionInfo.UserName" equals "test" (the value
of "User.Name") to keep the mock consistent (look for the SessionInfo.UserName
and User.Name fields in this file); also replace the literal AccessToken with a
clearly synthetic placeholder (e.g. "mock-access-token") to avoid accidental
secrets scanning confusion while keeping the test data deterministic.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #519 +/- ##
==========================================
Coverage 53.21% 53.21%
Complexity 518 518
==========================================
Files 69 69
Lines 2302 2302
Branches 329 329
==========================================
Hits 1225 1225
Misses 911 911
Partials 166 166
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Tests should be able to run on any system now for the emby client. A LIVE mode is available for testing against real server instances allowing verification of new server instances with the client. CAPTURE allows capturing of the new data from a server for use in testing later. MOCK uses the captured data and users a mockwebserver to provide responses.
b6a28c0 to
2803ad6
Compare
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@emby-lib/build.gradle.kts`:
- Around line 43-62: The release build's SERVER_URL string is missing a trailing
slash which will break Retrofit; update the release configuration in the
buildTypes block (getByName("release")) so the buildConfigField("String",
"SERVER_URL", ...) uses "http://localhost:8096/" (matching the debug serverUrl
variable) to ensure the base URL ends with a slash.
In
`@emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClientTest.kt`:
- Around line 94-107: In loginUserWithPassword, the local variable user (result
of client.allAvailableUsers().find { ... }) can be null; before using
user?.userId to build the prefs key and asserting the stored password, add a
null-check and fail fast (e.g., assertNotNull or call fail()) to stop the test
with a clear error if no matching user is found; update the test to assert the
user is non-null (reference: loginUserWithPassword, client.allAvailableUsers(),
user, prefs.getString).
In
`@emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyResponseCaptureInterceptor.kt`:
- Around line 14-21: The current file naming in EmbyResponseCaptureInterceptor
uses only request.url.encodedPath (fileName) so responses for the same path with
different query strings overwrite each other; update the logic that builds
fileName to include a sanitized representation of the query (e.g.,
request.url.encodedQuery or sorted query params) appended or hashed to the
encodedPath, ensure characters invalid in filenames are escaped, and write to
File(outputDir, newFileName). Also, before writing, check existing file
existence and emit a warning (or include a timestamp/hash) if overwriting would
occur to avoid silent loss; adjust the code that creates
file.parentFile?.mkdirs() and writes file.sink().buffer().use { ... } to use the
new fileName.
🧹 Nitpick comments (5)
emby-lib/build.gradle.kts (1)
43-46: Consider externalizing test credentials from the build script.Hardcoding
serverUrl,testUser, andtestPasswordin the build file makes it awkward for contributors to switch to LIVE/CAPTURE mode without editing a tracked file. Reading these fromlocal.propertiesor environment variables (with fallbacks to the current defaults) would be more flexible.♻️ Example approach
- val testMode = "MOCK" - val serverUrl = "http://localhost:8096/" - val testUser = "test" - val testPassword = "unknown" + val localProps = java.util.Properties().apply { + val f = rootProject.file("local.properties") + if (f.exists()) load(f.inputStream()) + } + val testMode = localProps.getProperty("test.mode", "MOCK") + val serverUrl = localProps.getProperty("test.serverUrl", "http://localhost:8096/") + val testUser = localProps.getProperty("test.user", "test") + val testPassword = localProps.getProperty("test.password", "unknown")emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClientTest.kt (3)
36-42:MockWebServeris started even in LIVE/CAPTURE modes where it is unused.In
LIVEorCAPTUREmode, the base URL isBuildConfig.SERVER_URLand the mock server is never queried, yet it's started and bound to a port. Consider conditionally starting it only inMOCKmode.♻️ Proposed fix
mockWebServer = MockWebServer() - mockWebServer.start(8096) + if (BuildConfig.TEST_MODE == "MOCK") { + mockWebServer.start() + } val baseUrl = when (BuildConfig.TEST_MODE) { "LIVE", "CAPTURE" -> BuildConfig.SERVER_URL else -> mockWebServer.url("/").toString() }Correspondingly, guard the teardown:
`@After` fun tearDown() { - mockWebServer.shutdown() + if (BuildConfig.TEST_MODE == "MOCK") { + mockWebServer.shutdown() + } }
63-70: MissingContent-Type: application/jsonheader on mock responses.Retrofit with Moshi may behave unexpectedly without a content-type header. Add
.addHeader("Content-Type", "application/json")to theMockResponse.♻️ Proposed fix
- mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(json)) + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(json) + )
201-214: Theauthenticate()helper will crash in MOCK mode — consider removing it.
authenticate()callsusers.first { !it.hasPassword() }, but both users in the mock data (emby_Users_Public.json) haveHasPassword: true. This would throwNoSuchElementException. The method is currently unused by any test, so it's effectively dead code.emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyResponseCaptureInterceptor.kt (1)
10-23: Unhandled I/O exceptions during capture will fail the HTTP request.If the file write on line 20 throws (e.g., disk full, permission denied), the exception propagates through the interceptor chain and fails the entire request. Since this is a diagnostic/capture tool, swallowing or logging the error and still returning the response would be more resilient.
♻️ Proposed fix
if (response.isSuccessful) { - val body = response.peekBody(Long.MAX_VALUE) - val fileName = request.url.encodedPath.trim('/').replace('/', '_') + ".json" - val file = File(outputDir, fileName) - - file.parentFile?.mkdirs() - file.sink().buffer().use { it.writeAll(body.source()) } + try { + val body = response.peekBody(Long.MAX_VALUE) + val fileName = request.url.encodedPath.trim('/').replace('/', '_') + ".json" + val file = File(outputDir, fileName) + + file.parentFile?.mkdirs() + file.sink().buffer().use { it.writeAll(body.source()) } + } catch (e: Exception) { + println("Warning: Failed to capture response for ${request.url}: ${e.message}") + } }
| val testMode = "MOCK" | ||
| val serverUrl = "http://localhost:8096/" | ||
| val testUser = "test" | ||
| val testPassword = "unknown" | ||
|
|
||
| buildTypes { | ||
| getByName("debug") { | ||
| buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"") | ||
| buildConfigField("String", "TEST_MODE", "\"$testMode\"") | ||
| buildConfigField("String", "SERVER_URL", "\"$serverUrl\"") | ||
| buildConfigField("String", "TEST_USER", "\"$testUser\"") | ||
| buildConfigField("String", "TEST_PASSWORD", "\"$testPassword\"") | ||
| } | ||
|
|
||
| getByName("release") { | ||
| buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"") | ||
| buildConfigField("String", "TEST_MODE", "\"MOCK\"") | ||
| buildConfigField("String", "SERVER_URL", "\"http://localhost:8096\"") | ||
| buildConfigField("String", "TEST_USER", "\"test\"") | ||
| buildConfigField("String", "TEST_PASSWORD", "\"unknown\"") |
There was a problem hiding this comment.
Trailing-slash inconsistency between debug and release SERVER_URL.
Debug serverUrl on line 44 ends with / ("http://localhost:8096/"), but the release SERVER_URL on line 60 does not ("http://localhost:8096"). Retrofit requires the base URL to end with /; a missing trailing slash will cause an IllegalArgumentException at runtime.
🐛 Proposed fix
getByName("release") {
buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"")
buildConfigField("String", "TEST_MODE", "\"MOCK\"")
- buildConfigField("String", "SERVER_URL", "\"http://localhost:8096\"")
+ buildConfigField("String", "SERVER_URL", "\"http://localhost:8096/\"")
buildConfigField("String", "TEST_USER", "\"test\"")
buildConfigField("String", "TEST_PASSWORD", "\"unknown\"")
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val testMode = "MOCK" | |
| val serverUrl = "http://localhost:8096/" | |
| val testUser = "test" | |
| val testPassword = "unknown" | |
| buildTypes { | |
| getByName("debug") { | |
| buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"") | |
| buildConfigField("String", "TEST_MODE", "\"$testMode\"") | |
| buildConfigField("String", "SERVER_URL", "\"$serverUrl\"") | |
| buildConfigField("String", "TEST_USER", "\"$testUser\"") | |
| buildConfigField("String", "TEST_PASSWORD", "\"$testPassword\"") | |
| } | |
| getByName("release") { | |
| buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"") | |
| buildConfigField("String", "TEST_MODE", "\"MOCK\"") | |
| buildConfigField("String", "SERVER_URL", "\"http://localhost:8096\"") | |
| buildConfigField("String", "TEST_USER", "\"test\"") | |
| buildConfigField("String", "TEST_PASSWORD", "\"unknown\"") | |
| val testMode = "MOCK" | |
| val serverUrl = "http://localhost:8096/" | |
| val testUser = "test" | |
| val testPassword = "unknown" | |
| buildTypes { | |
| getByName("debug") { | |
| buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"") | |
| buildConfigField("String", "TEST_MODE", "\"$testMode\"") | |
| buildConfigField("String", "SERVER_URL", "\"$serverUrl\"") | |
| buildConfigField("String", "TEST_USER", "\"$testUser\"") | |
| buildConfigField("String", "TEST_PASSWORD", "\"$testPassword\"") | |
| } | |
| getByName("release") { | |
| buildConfigField("String", "CLIENT_VERSION", "\"${libs.versions.appVersion.get()}\"") | |
| buildConfigField("String", "TEST_MODE", "\"MOCK\"") | |
| buildConfigField("String", "SERVER_URL", "\"http://localhost:8096/\"") | |
| buildConfigField("String", "TEST_USER", "\"test\"") | |
| buildConfigField("String", "TEST_PASSWORD", "\"unknown\"") | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@emby-lib/build.gradle.kts` around lines 43 - 62, The release build's
SERVER_URL string is missing a trailing slash which will break Retrofit; update
the release configuration in the buildTypes block (getByName("release")) so the
buildConfigField("String", "SERVER_URL", ...) uses "http://localhost:8096/"
(matching the debug serverUrl variable) to ensure the base URL ends with a
slash.
| @Test | ||
| fun loginUserWithPassword() { | ||
| // This needs to be the url to the server you want to test against | ||
| client.updateBaseUrl("http://yourserver:yourport") | ||
| enqueueResponse("emby_Users_Public.json") | ||
| enqueueResponse("emby_Users_Public.json") | ||
| enqueueResponse("emby_Users_AuthenticateByName.json") | ||
|
|
||
| val users = client.allAvailableUsers() | ||
| val user = users.first { it.hasPassword() } | ||
| client.authenticateUser(user, "unknownpassword") | ||
| val user = users.find { user -> user.userName == BuildConfig.TEST_USER } | ||
|
|
||
| authenticateWithPassword() | ||
|
|
||
| val password = prefs.getString("emby_${user.userId}_password", null) | ||
| assertThat(password).isNotNull().isEqualTo("unknownpassword") | ||
| val password = prefs.getString("emby_${user?.userId}_password", null) | ||
| assertThat(password).isNotNull().isEqualTo(BuildConfig.TEST_PASSWORD) | ||
| } |
There was a problem hiding this comment.
Null-unsafe usage of user in loginUserWithPassword.
On line 101, user is the result of .find { ... } which can be null. Unlike authenticateWithPassword() (lines 219-221) which has an explicit null check with fail(), here user?.userId silently produces "emby_null_password" as the prefs key, leading to a confusing assertion failure instead of a clear error.
🛡️ Proposed fix — fail fast on null
val users = client.allAvailableUsers()
- val user = users.find { user -> user.userName == BuildConfig.TEST_USER }
-
- authenticateWithPassword()
+ val user = users.find { user -> user.userName == BuildConfig.TEST_USER }
+ ?: fail("Unable to find user '${BuildConfig.TEST_USER}' in public users list")
- val password = prefs.getString("emby_${user?.userId}_password", null)
+ authenticateWithPassword()
+
+ val password = prefs.getString("emby_${user.userId}_password", null)
assertThat(password).isNotNull().isEqualTo(BuildConfig.TEST_PASSWORD)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Test | |
| fun loginUserWithPassword() { | |
| // This needs to be the url to the server you want to test against | |
| client.updateBaseUrl("http://yourserver:yourport") | |
| enqueueResponse("emby_Users_Public.json") | |
| enqueueResponse("emby_Users_Public.json") | |
| enqueueResponse("emby_Users_AuthenticateByName.json") | |
| val users = client.allAvailableUsers() | |
| val user = users.first { it.hasPassword() } | |
| client.authenticateUser(user, "unknownpassword") | |
| val user = users.find { user -> user.userName == BuildConfig.TEST_USER } | |
| authenticateWithPassword() | |
| val password = prefs.getString("emby_${user.userId}_password", null) | |
| assertThat(password).isNotNull().isEqualTo("unknownpassword") | |
| val password = prefs.getString("emby_${user?.userId}_password", null) | |
| assertThat(password).isNotNull().isEqualTo(BuildConfig.TEST_PASSWORD) | |
| } | |
| `@Test` | |
| fun loginUserWithPassword() { | |
| enqueueResponse("emby_Users_Public.json") | |
| enqueueResponse("emby_Users_Public.json") | |
| enqueueResponse("emby_Users_AuthenticateByName.json") | |
| val users = client.allAvailableUsers() | |
| val user = users.find { user -> user.userName == BuildConfig.TEST_USER } | |
| ?: fail("Unable to find user '${BuildConfig.TEST_USER}' in public users list") | |
| authenticateWithPassword() | |
| val password = prefs.getString("emby_${user.userId}_password", null) | |
| assertThat(password).isNotNull().isEqualTo(BuildConfig.TEST_PASSWORD) | |
| } |
🤖 Prompt for AI Agents
In
`@emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClientTest.kt`
around lines 94 - 107, In loginUserWithPassword, the local variable user (result
of client.allAvailableUsers().find { ... }) can be null; before using
user?.userId to build the prefs key and asserting the stored password, add a
null-check and fail fast (e.g., assertNotNull or call fail()) to stop the test
with a clear error if no matching user is found; update the test to assert the
user is non-null (reference: loginUserWithPassword, client.allAvailableUsers(),
user, prefs.getString).
| if (response.isSuccessful) { | ||
| val body = response.peekBody(Long.MAX_VALUE) | ||
| val fileName = request.url.encodedPath.trim('/').replace('/', '_') + ".json" | ||
| val file = File(outputDir, fileName) | ||
|
|
||
| file.parentFile?.mkdirs() | ||
| file.sink().buffer().use { it.writeAll(body.source()) } | ||
| } |
There was a problem hiding this comment.
Filename collisions when the same path is called with different query parameters.
The filename is derived solely from encodedPath, ignoring query parameters. In CAPTURE mode, multiple requests to the same path with different query strings (e.g., /emby/Users/{id}/Items?IncludeItemTypes=Movie vs ...=Episode) will silently overwrite each other, and only the last response survives.
Consider incorporating distinguishing query parameters into the filename, or logging a warning when a file is overwritten.
🤖 Prompt for AI Agents
In
`@emby-lib/src/test/kotlin/us/nineworlds/serenity/emby/server/api/EmbyResponseCaptureInterceptor.kt`
around lines 14 - 21, The current file naming in EmbyResponseCaptureInterceptor
uses only request.url.encodedPath (fileName) so responses for the same path with
different query strings overwrite each other; update the logic that builds
fileName to include a sanitized representation of the query (e.g.,
request.url.encodedQuery or sorted query params) appended or hashed to the
encodedPath, ensure characters invalid in filenames are escaped, and write to
File(outputDir, newFileName). Also, before writing, check existing file
existence and emit a warning (or include a timestamp/hash) if overwriting would
occur to avoid silent loss; adjust the code that creates
file.parentFile?.mkdirs() and writes file.sink().buffer().use { ... } to use the
new fileName.



Tests should be able to run on any system now for the emby client. A LIVE mode is available for testing against real server instances allowing verification of new server instances with the client. CAPTURE allows capturing of the new data from a server for use in testing later.
MOCK uses the captured data and users a mockwebserver to provide responses.
Summary by CodeRabbit
Documentation
Tests
Chores