Skip to content

test: Update Tests so they can run in LIVE, CAPTURE or MOCK modes#519

Merged
kingargyle merged 1 commit intodevelopfrom
dac_update_emby_tests
Feb 16, 2026
Merged

test: Update Tests so they can run in LIVE, CAPTURE or MOCK modes#519
kingargyle merged 1 commit intodevelopfrom
dac_update_emby_tests

Conversation

@kingargyle
Copy link
Contributor

@kingargyle kingargyle commented Feb 16, 2026

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

    • Added a new mandatory pre-change checklist item and renumbered the checklist.
  • Tests

    • Enhanced test infrastructure with configurable test modes, mock server support, response-capture tooling, and updated build test settings.
    • Added comprehensive mock data fixtures for authentication, users, views, and genres.
  • Chores

    • Removed an obsolete paging support plan document.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Walkthrough

Adds 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

Cohort / File(s) Summary
Repository Docs & Build Config
AGENTS.md, emby-lib/build.gradle.kts
Prepended a "MUST ASK BEFORE making any changes." directive to AGENTS.md; added test resource src dir, test-mode build constants (TEST_MODE, SERVER_URL, TEST_USER, TEST_PASSWORD) to BuildConfig for debug/release and added MockWebServer test dependency.
HTTP Client
emby-lib/src/main/kotlin/.../EmbyAPIClient.kt
Made constructor @JvmOverloads and added additionalInterceptors: List<Interceptor> = emptyList(); uses RetrofitUrlManager with an OkHttpClient.Builder, wires in additional interceptors before building the client.
Test Code & Utilities
emby-lib/src/test/kotlin/.../EmbyAPIClientTest.kt, emby-lib/src/test/kotlin/.../EmbyResponseCaptureInterceptor.kt
Refactored tests to run against MockWebServer with mode-driven base URL (LIVE/CAPTURE/MOCK), lifecycle & response enqueueing, and conditional interceptors; added EmbyResponseCaptureInterceptor to write successful responses to files for capture mode.
Test Fixtures
emby-lib/src/test/resources/mock-data/emby_Genres.json, .../emby_Users_AuthenticateByName.json, .../emby_Users_Public.json, .../emby_Users_*_Views.json
Added multiple mock JSON response files for users, authentication, views, and genres used by the updated tests.
Deleted Doc
paging_support_plan.md
Removed the paging_support_plan.md planning document from the repo.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through tests with nimble paws,
MockWebServer hummed without a pause,
Interceptors caught each streaming bite,
Carrots of JSON in the soft moonlight,
BuildConfig snug — the burrow's right. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: updating tests to support LIVE, CAPTURE, and MOCK execution modes, which directly reflects the core objective and file changes throughout the PR.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dac_update_emby_tests

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

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

Copy link
Contributor

@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: 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=3 vs /Items?parentId=5) will overwrite each other since only encodedPath is 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 have HasPassword: true). Consider removing it.

emby-lib/build.gradle.kts (2)

43-54: Consider reading test credentials from environment variables or gradle.properties.

Hardcoding testMode, serverUrl, testUser, and testPassword in build.gradle.kts requires editing the build file to switch between MOCK/LIVE/CAPTURE modes. Using findProperty() or System.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, and TEST_PASSWORD are test infrastructure fields that have no purpose in the release build type. While harmless, they add noise to the release BuildConfig.

Comment on lines +36 to +37
mockWebServer = MockWebServer()
mockWebServer.start(8096)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +88 to +107
"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"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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
Copy link

codecov bot commented Feb 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.21%. Comparing base (9ff3d52) to head (2803ad6).
⚠️ Report is 2 commits behind head on develop.

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           
Flag Coverage Δ
unittests 53.21% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@kingargyle kingargyle force-pushed the dac_update_emby_tests branch from b6a28c0 to 2803ad6 Compare February 16, 2026 03:43
@sonarqubecloud
Copy link

Copy link
Contributor

@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: 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, and testPassword in the build file makes it awkward for contributors to switch to LIVE/CAPTURE mode without editing a tracked file. Reading these from local.properties or 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: MockWebServer is started even in LIVE/CAPTURE modes where it is unused.

In LIVE or CAPTURE mode, the base URL is BuildConfig.SERVER_URL and the mock server is never queried, yet it's started and bound to a port. Consider conditionally starting it only in MOCK mode.

♻️ 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: Missing Content-Type: application/json header on mock responses.

Retrofit with Moshi may behave unexpectedly without a content-type header. Add .addHeader("Content-Type", "application/json") to the MockResponse.

♻️ Proposed fix
-            mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(json))
+            mockWebServer.enqueue(
+                MockResponse()
+                    .setResponseCode(200)
+                    .addHeader("Content-Type", "application/json")
+                    .setBody(json)
+            )

201-214: The authenticate() helper will crash in MOCK mode — consider removing it.

authenticate() calls users.first { !it.hasPassword() }, but both users in the mock data (emby_Users_Public.json) have HasPassword: true. This would throw NoSuchElementException. 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}")
+            }
         }

Comment on lines +43 to +62
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\"")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines 94 to 107
@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)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
@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).

Comment on lines +14 to +21
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()) }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@kingargyle kingargyle merged commit 71a8e8a into develop Feb 16, 2026
5 checks passed
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