Skip to content

Fix interrupted response when provided an unknown user#5354

Draft
jrh3k5 wants to merge 1 commit intoOmbi-app:developfrom
jrh3k5:user-unknown-error
Draft

Fix interrupted response when provided an unknown user#5354
jrh3k5 wants to merge 1 commit intoOmbi-app:developfrom
jrh3k5:user-unknown-error

Conversation

@jrh3k5
Copy link
Contributor

@jrh3k5 jrh3k5 commented Mar 21, 2026

The handler code change and unit tests were written by GitHub Copilot. Subsequent edits, PR description, and manual verification were done by me.

📝 Description

If a UserName header that identifies a user not known to Ombi is supplied, it un-gracefully closes the connection, causing errors like this one that I encountered in a Node app talking to Ombi:

AxiosError: stream has been aborted
    at IncomingMessage.handlerStreamAborted (file:///home/node/app/node_modules/axios/lib/adapters/http.js:815:25)

(all API keys mentioned below were generated only for the local development instance of Ombi, not the one my actual Ombi instance uses)

This is because, when the app does not recognize a username, it writes a response but then continues invoking the handler chain. That causes the transfer closed with outstanding read data remaining error when invoking with curl:

curl -X POST -v http://localhost:3577/api/v2/search/multi/seven%20samurai \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "ApiKey: 776b7bbab0034627ab5a9c59fa166914" \
  -H "UserName: doesnotexist" \
  -d '{"movies":true}'

Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:3577 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3577...
* Connected to localhost (::1) port 3577
> POST /api/v2/search/multi/seven%20samurai HTTP/1.1
> Host: localhost:3577
> User-Agent: curl/8.7.1
> Accept: application/json
> Content-Type: application/json
> ApiKey: 776b7bbab0034627ab5a9c59fa166914
> UserName: doesnotexist
> Content-Length: 15
> 
* upload completely sent off: 15 bytes
< HTTP/1.1 401 Unauthorized
< Date: Sat, 21 Mar 2026 18:09:28 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* transfer closed with outstanding read data remaining
* Closing connection
curl: (18) transfer closed with outstanding read data remaining
Invalid User%

It also caused this error in the server:

fail: Ombi.ErrorHandlingMiddleware[0]
      Something bad happened, ErrorMiddleware caught this
      System.InvalidOperationException: StatusCode cannot be set because the response has already started.
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
         at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)
         at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleChallengeAsync(AuthenticationProperties properties)
         at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.ChallengeAsync(AuthenticationProperties properties)
         at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
         at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.<>c__DisplayClass0_0.<<HandleAsync>g__Handle|0>d.MoveNext()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Ombi.ApiKeyMiddlewear.ValidateApiKey(HttpContext context, RequestDelegate next, String key) in /Users/jrh3k5/src/Ombi-app/Ombi/src/Ombi/Middleware/ApiKeyMiddlewear.cs:line 121
         at Ombi.ApiKeyMiddlewear.Invoke(HttpContext context) in /Users/jrh3k5/src/Ombi-app/Ombi/src/Ombi/Middleware/ApiKeyMiddlewear.cs:line 34
         at Ombi.ErrorHandlingMiddleware.Invoke(HttpContext context) in /Users/jrh3k5/src/Ombi-app/Ombi/src/Ombi/Middleware/ErrorHandlingMiddlewear.cs:line 24
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNK7EJC5U56A", Request id "0HNK7EJC5U56A:00000001": An unhandled exception was thrown by the application.
      System.InvalidOperationException: Headers are read-only, response has already started.
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_ContentType(StringValues value)
         at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
         at Ombi.ErrorHandlingMiddleware.HandleExceptionAsync(HttpContext context, Exception exception) in /Users/jrh3k5/src/Ombi-app/Ombi/src/Ombi/Middleware/ErrorHandlingMiddlewear.cs:line 51
         at Ombi.ErrorHandlingMiddleware.Invoke(HttpContext context) in /Users/jrh3k5/src/Ombi-app/Ombi/src/Ombi/Middleware/ErrorHandlingMiddlewear.cs:line 28
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

With this change in place, the curl command yields a non-erroring response (HTTP 401 response notwithstanding):

curl -X POST -v http://localhost:3577/api/v2/search/multi/seven%20samurai \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "ApiKey: 776b7bbab0034627ab5a9c59fa166914" \
  -H "UserName: doesnotexist" \
  -d '{"movies":true}'

Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:3577 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3577...
* Connected to localhost (::1) port 3577
> POST /api/v2/search/multi/seven%20samurai HTTP/1.1
> Host: localhost:3577
> User-Agent: curl/8.7.1
> Accept: application/json
> Content-Type: application/json
> ApiKey: 776b7bbab0034627ab5a9c59fa166914
> UserName: doesnotexist
> Content-Length: 15
> 
* upload completely sent off: 15 bytes
< HTTP/1.1 401 Unauthorized
< Date: Sat, 21 Mar 2026 18:14:02 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
Invalid User%

In the course of drafting the PR, I received automated feedback that there remained a hole in the logic for when there was no username provided, so I addressed that, too (and provided a unit test), by nesting the username validation within the else block that triggers if the username is provided and not blank.

I then addressed all highlighted warnings (pre-existing and newly-introduced by myself or Copilot).

🔗 Related Issues

🧪 Testing

I made sure the new unit test failed its assertion that the handler was not invoked by reverting the change to the handler.

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed
  • No breaking changes

📸 Screenshots (if applicable)

N/A

📋 Checklist

  • My code follows the project's coding standards
  • I have mentioned if this is a vibe coded PR
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

🎯 Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring

📚 Additional Notes

Summary by CodeRabbit

  • Bug Fixes

    • API-key impersonation now stops and returns 401 with "Invalid User" when a named user does not exist, preventing downstream processing.
  • Tests

    • Added tests for failed impersonation and for API-key flow when no user-name header is present (ensures request proceeds and identity is set to "API").
    • Test helpers updated to source the auth token from environment storage for Cypress commands and requests.

@tidusjar
Copy link
Member

tidusjar commented Mar 21, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Stops middleware from continuing the request pipeline when an API-key impersonation header names a nonexistent user; adds unit tests for that case and for API-key auth without UserName; updates Cypress test helpers to source the Bearer token from Cypress.env('access_token').

Changes

Cohort / File(s) Summary
API Key Middleware
src/Ombi/Middleware/ApiKeyMiddlewear.cs
ValidateApiKey now writes "Invalid User" and returns immediately when a UserName header is present but no matching user is found, preventing downstream invocation.
Middleware Tests
src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs
Added ValidateApiKey_InvalidUser and ValidateApiKey_NoUserNameHeader tests; switched request header setup to indexer assignment, adjusted MockUserManager construction, and added #nullable toggles around AutoMocker service resolution.
Cypress support commands
tests/cypress/support/commands.ts, tests/cypress/support/plex-settings.commands.ts, tests/cypress/support/request.commands.ts
All Cypress request helpers now store/read the auth token from Cypress.env('access_token') (e.g., loginWithCreds sets it; other commands use it for Authorization: Bearer ...).
sequenceDiagram
  autonumber
  participant C as Client
  participant M as ApiKeyMiddleware
  participant S as ISettingsService/OmbiSettings
  participant U as OmbiUserManager
  participant D as Downstream (RequestDelegate)

  C->>M: HTTP request (ApiKey + optional UserName headers)
  M->>S: GetSettingsAsync()
  S-->>M: OmbiSettings (ApiKey matches)
  alt UserName header present
    M->>U: Find user by UserName
    U-->>M: No matching user
    M-->>C: Write 401 "Invalid User" and return
    Note over M,D: Downstream not invoked
  else No UserName header
    M->>M: Create API identity (Name = "API")
    M-->>D: Invoke downstream
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniffed the headers in the night,
A missing name — I set things right.
If phantom names come calling through,
I stop the hop and guard the crew. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title does not follow the Conventional Commits specification, lacking a type prefix (e.g., 'fix:', 'feat:') required by the title check requirements. Update the title to 'fix: prevent interrupted response when unknown user is provided' to comply with Conventional Commits format.
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 (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and largely complete, covering all major template sections including description, testing checklist, type of change, and additional context with detailed error traces.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit 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.

@jrh3k5 jrh3k5 mentioned this pull request Mar 21, 2026
19 tasks
Copy link

@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.

🧹 Nitpick comments (2)
src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs (1)

97-126: Good test coverage for the fix.

The test correctly validates that:

  1. A 401 status is returned for an invalid user
  2. The response body contains "Invalid User"
  3. The downstream RequestDelegate is never invoked

Minor hygiene suggestion: consider wrapping the StreamReader in a using statement to ensure proper disposal.

♻️ Optional: dispose StreamReader
             Assert.That(context.Response.StatusCode, Is.EqualTo(401));
             bodyStream.Position = 0;
-            var reader = new StreamReader(bodyStream);
-            var body = await reader.ReadToEndAsync();
+            using var reader = new StreamReader(bodyStream);
+            var body = await reader.ReadToEndAsync();
             Assert.That(body, Is.EqualTo("Invalid User"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs` around lines 97 - 126,
The StreamReader created in the ValidateApiKey_InvalidUser test is not disposed;
wrap the StreamReader (constructed from bodyStream) in a using (or use await
using) to ensure it is properly disposed after reading the response body in the
test method ValidateApiKey_InvalidUser so resources are released and test
hygiene is improved.
src/Ombi/Middleware/ApiKeyMiddlewear.cs (1)

104-116: Pre-existing logic concern: empty username handling may have unintended behaviour.

When username.IsNullOrEmpty() is true, UseApiUser is called, but execution then continues past the if-else to lines 113-116 where user lookup occurs. Since no user will match an empty/null normalised username, this will result in a 401 "Invalid User" response.

This differs from the no-header case (lines 128-130) which calls UseApiUser and then proceeds to next.Invoke. If an empty UserName header should behave the same as no header, consider adding a return or restructuring the flow.

This is pre-existing and out of scope for this PR, but worth noting for future consideration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Ombi/Middleware/ApiKeyMiddlewear.cs` around lines 104 - 116, When
username.IsNullOrEmpty() is true the code calls UseApiUser(context) but then
continues to run the user lookup (um.Users.FirstOrDefaultAsync) which will fail
and return 401; change the control flow so that when username.IsNullOrEmpty()
you stop further processing and behave like the no-header case — either
return/await next.Invoke after calling UseApiUser(context) or set username to
the API user’s normalized name before the lookup. Update the block around
username.IsNullOrEmpty(), UseApiUser(context), and the subsequent um
(OmbiUserManager) Users.FirstOrDefaultAsync call to ensure no lookup occurs for
an empty header and execution continues to next.Invoke.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs`:
- Around line 97-126: The StreamReader created in the ValidateApiKey_InvalidUser
test is not disposed; wrap the StreamReader (constructed from bodyStream) in a
using (or use await using) to ensure it is properly disposed after reading the
response body in the test method ValidateApiKey_InvalidUser so resources are
released and test hygiene is improved.

In `@src/Ombi/Middleware/ApiKeyMiddlewear.cs`:
- Around line 104-116: When username.IsNullOrEmpty() is true the code calls
UseApiUser(context) but then continues to run the user lookup
(um.Users.FirstOrDefaultAsync) which will fail and return 401; change the
control flow so that when username.IsNullOrEmpty() you stop further processing
and behave like the no-header case — either return/await next.Invoke after
calling UseApiUser(context) or set username to the API user’s normalized name
before the lookup. Update the block around username.IsNullOrEmpty(),
UseApiUser(context), and the subsequent um (OmbiUserManager)
Users.FirstOrDefaultAsync call to ensure no lookup occurs for an empty header
and execution continues to next.Invoke.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1239053-e6dc-44d6-9d43-09eee2e32a1a

📥 Commits

Reviewing files that changed from the base of the PR and between 6752a8a and fba1647.

📒 Files selected for processing (2)
  • src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs
  • src/Ombi/Middleware/ApiKeyMiddlewear.cs

@tidusjar
Copy link
Member

Thanks for the contribution!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes API key authentication middleware so that when a request attempts to impersonate an unknown user (via the UserName header), the middleware stops processing instead of continuing the request pipeline.

Changes:

  • Stop request pipeline execution on “Invalid User” during API key validation (return instead of invoking next).
  • Add a unit test asserting 401 + body message and that next is not invoked for an invalid impersonated user.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Ombi/Middleware/ApiKeyMiddlewear.cs Halts middleware execution when an impersonated user cannot be resolved.
src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs Adds coverage for the invalid-user impersonation path (ensures 401 + no downstream invocation).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@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.

🧹 Nitpick comments (1)
src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs (1)

111-115: Consider extracting duplicated settings mock setup into a helper.

The ISettingsService<OmbiSettings> arrangement is repeated in multiple tests; a small helper would reduce boilerplate and keep tests easier to evolve.

Also applies to: 145-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs` around lines 111 - 115,
Extract the duplicated setup for ISettingsService<OmbiSettings> into a single
helper method (e.g., CreateSettingsServiceMock or SetupOmbiSettingsMock) inside
the ApiKeyMiddlewearTests class that returns a
Mock<ISettingsService<OmbiSettings>> (or configures the _mocker to return the
mock), initialise it with new OmbiSettings { ApiKey = "validkey" }, and update
the tests that currently create settingsMock (the occurrences around
settingsMock and the _mocker.Setup for IServiceProvider) to call that helper
instead to remove boilerplate and centralise future changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs`:
- Around line 111-115: Extract the duplicated setup for
ISettingsService<OmbiSettings> into a single helper method (e.g.,
CreateSettingsServiceMock or SetupOmbiSettingsMock) inside the
ApiKeyMiddlewearTests class that returns a Mock<ISettingsService<OmbiSettings>>
(or configures the _mocker to return the mock), initialise it with new
OmbiSettings { ApiKey = "validkey" }, and update the tests that currently create
settingsMock (the occurrences around settingsMock and the _mocker.Setup for
IServiceProvider) to call that helper instead to remove boilerplate and
centralise future changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e55f3612-a5b7-43ec-afd6-c5762e77f276

📥 Commits

Reviewing files that changed from the base of the PR and between 73782a5 and a6260ec.

📒 Files selected for processing (2)
  • src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs
  • src/Ombi/Middleware/ApiKeyMiddlewear.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Ombi/Middleware/ApiKeyMiddlewear.cs

@jrh3k5 jrh3k5 marked this pull request as ready for review March 21, 2026 22:18
@qodo-code-review
Copy link

Review Summary by Qodo

Fix handler chain continuation on unknown user rejection

🐞 Bug fix 🧪 Tests

Grey Divider

Walkthroughs

Description
• Fix interrupted response when unknown user provided via UserName header
• Return early instead of continuing handler chain after rejection
• Add unit tests for invalid user and missing username scenarios
• Update test assertions and nullable annotations for C# 8.0 compatibility
Diagram
flowchart LR
  A["Request with UserName header"] --> B{"Username provided?"}
  B -->|No| C["Use API user"]
  B -->|Yes| D{"User exists?"}
  D -->|Yes| E["Set user context"]
  D -->|No| F["Return 401 Unauthorized"]
  C --> G["Continue handler chain"]
  E --> G
  F --> H["Stop execution"]
Loading

Grey Divider

File Changes

1. src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs 🧪 Tests +73/-6

Add tests and update test syntax for middleware validation

• Added using statements for ISettingsService and MemoryStream
• Updated header assignment syntax from .Add() to direct assignment
• Converted list initialization to collection expression syntax
• Added #nullable enable/disable directives for type safety
• Added two new unit tests: ValidateApiKey_InvalidUser and ValidateApiKey_NoUserNameHeader

src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs


2. src/Ombi/Middleware/ApiKeyMiddlewear.cs 🐞 Bug fix +14/-14

Return early on invalid user instead of continuing chain

• Restructured username validation logic to check if username is provided
• Moved user lookup and validation inside the else block when username is provided
• Changed from await next.Invoke(context) to return statement when user is not found
• Prevents handler chain continuation after sending 401 Unauthorized response

src/Ombi/Middleware/ApiKeyMiddlewear.cs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 21, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Mocked next returns null🐞 Bug ✓ Correctness
Description
ValidateApiKey_NoUserNameHeader constructs a Mock<RequestDelegate> without configuring a Task return
value; ApiKeyMiddlewear awaits next.Invoke(context) on this code path, which can cause a
NullReferenceException and fail the test. This prevents the test from reliably validating the
intended behavior (API user is set and pipeline continues).
Code

src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs[R151-158]

+            var nextMock = new Mock<RequestDelegate>();
+            var subject = new ApiKeyMiddlewear(nextMock.Object);
+
+            await subject.Invoke(context);
+
+            Assert.That(context.Response.StatusCode, Is.EqualTo(200)); // Default status
+            Assert.That(context.User.Identity.Name, Is.EqualTo("API"));
+            nextMock.Verify(x => x.Invoke(It.IsAny<HttpContext>()), Times.Once);
Evidence
The test uses a Moq mock for RequestDelegate without any Setup/Returns, then calls into middleware
expecting the delegate to be invoked. In the middleware, when the API key is valid and there is no
UserName header, it still executes await next.Invoke(context) after setting the API principal, so
the unconfigured delegate return value is awaited.

src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs[138-159]
src/Ombi/Middleware/ApiKeyMiddlewear.cs[88-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ValidateApiKey_NoUserNameHeader` uses `new Mock&amp;lt;RequestDelegate&amp;gt;()` but never configures it to return a non-null `Task`. The middleware awaits `next.Invoke(context)` on the valid API key path, so the test can throw instead of asserting behavior.
### Issue Context
This affects the new test added in this PR; it should verify the pipeline continues (next called) after setting the API principal.
### Fix Focus Areas
- src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs[138-159]
### Concrete fix
In `ValidateApiKey_NoUserNameHeader` (and optionally also in `ValidateApiKey_InvalidUser` to avoid NRE if behavior regresses), configure:
- `nextMock.Setup(n =&amp;gt; n.Invoke(It.IsAny&amp;lt;HttpContext&amp;gt;())).Returns(Task.CompletedTask);`
(Alternatively avoid Moq entirely: `RequestDelegate next = _ =&amp;gt; Task.CompletedTask;` and use a boolean flag to assert it was called.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link

@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.

🧹 Nitpick comments (1)
src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs (1)

139-161: Add a companion test for empty UserName header

Please add a test where UserName header is present but empty ("" or whitespace) to cover the username.IsNullOrEmpty() branch in ApiKeyMiddlewear.ValidateApiKey and prevent regressions around the recent null/blank handling changes.

Proposed test addition
+        [Test]
+        public async Task ValidateApiKey_EmptyUserNameHeader_UsesApiUser()
+        {
+            var context = GetContext();
+            context.Request.Path = "/api";
+            context.Request.Headers["ApiKey"] = "validkey";
+            context.Request.Headers["UserName"] = "";
+
+            var settingsMock = new Mock<ISettingsService<OmbiSettings>>();
+            settingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new OmbiSettings { ApiKey = "validkey" });
+#nullable enable
+            _mocker.Setup<IServiceProvider, object?>(x => x.GetService(typeof(ISettingsService<OmbiSettings>))).Returns(settingsMock.Object);
+#nullable disable
+
+            var nextMock = new Mock<RequestDelegate>();
+            nextMock.Setup(n => n.Invoke(It.IsAny<HttpContext>())).Returns(Task.CompletedTask);
+            var subject = new ApiKeyMiddlewear(nextMock.Object);
+
+            await subject.Invoke(context);
+
+            Assert.That(context.Response.StatusCode, Is.EqualTo(200));
+            Assert.That(context.User.Identity.Name, Is.EqualTo("API"));
+            nextMock.Verify(x => x.Invoke(It.IsAny<HttpContext>()), Times.Once);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs` around lines 139 - 161,
Add a new unit test (e.g., ValidateApiKey_EmptyUserNameHeader) that mirrors
ValidateApiKey_NoUserNameHeader but sets the "UserName" request header to an
empty string or whitespace (e.g., "" or "   ") to exercise the
username.IsNullOrEmpty() branch in ApiKeyMiddlewear.ValidateApiKey; keep the
"ApiKey" header and ISettingsService<OmbiSettings> mock returning the valid key,
use the same RequestDelegate mock and ApiKeyMiddlewear instantiation, await
subject.Invoke(context), and assert response.StatusCode == 200,
context.User.Identity.Name == "API", and that nextMock.Invoke was called once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs`:
- Around line 139-161: Add a new unit test (e.g.,
ValidateApiKey_EmptyUserNameHeader) that mirrors ValidateApiKey_NoUserNameHeader
but sets the "UserName" request header to an empty string or whitespace (e.g.,
"" or "   ") to exercise the username.IsNullOrEmpty() branch in
ApiKeyMiddlewear.ValidateApiKey; keep the "ApiKey" header and
ISettingsService<OmbiSettings> mock returning the valid key, use the same
RequestDelegate mock and ApiKeyMiddlewear instantiation, await
subject.Invoke(context), and assert response.StatusCode == 200,
context.User.Identity.Name == "API", and that nextMock.Invoke was called once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e1a5826-d299-4254-9d83-f20474628097

📥 Commits

Reviewing files that changed from the base of the PR and between a6260ec and de6d464.

📒 Files selected for processing (1)
  • src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs

@jrh3k5 jrh3k5 marked this pull request as draft March 21, 2026 23:59
Copy link

@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.

🧹 Nitpick comments (1)
tests/cypress/support/request.commands.ts (1)

21-21: Consider adding a guard for missing token (optional).

Unlike createUser in commands.ts, these commands don't validate that the token exists before making the request. If called before cy.login(), the request will silently send "Bearer undefined". A guard similar to createUser would provide clearer error messages during test debugging.

💡 Example guard pattern (apply to each command if desired)
 Cypress.Commands.add('requestGenericMovie', () => {
+    const token = Cypress.env('access_token');
+    if (!token) {
+        throw new Error('No authentication token found. Please login first.');
+    }
     cy.request({
         method: 'POST',
         url: '/api/v1/request/movie',
         body: {
             TheMovieDbId: 299536
         },
         headers: {
-            'Authorization': 'Bearer ' + Cypress.env('access_token'),
+            'Authorization': 'Bearer ' + token,
         }
     })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cypress/support/request.commands.ts` at line 21, Add a guard that
validates Cypress.env('access_token') before adding the Authorization header to
prevent sending "Bearer undefined"; check the token at the start of each command
that sets 'Authorization' (the spot that currently builds 'Authorization':
'Bearer ' + Cypress.env('access_token') ) and throw or fail the test with a
clear message (similar to the createUser guard in commands.ts) when the token is
missing so callers get an immediate, descriptive error instead of a malformed
header.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/cypress/support/request.commands.ts`:
- Line 21: Add a guard that validates Cypress.env('access_token') before adding
the Authorization header to prevent sending "Bearer undefined"; check the token
at the start of each command that sets 'Authorization' (the spot that currently
builds 'Authorization': 'Bearer ' + Cypress.env('access_token') ) and throw or
fail the test with a clear message (similar to the createUser guard in
commands.ts) when the token is missing so callers get an immediate, descriptive
error instead of a malformed header.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b19d71c5-5afe-42d6-a24c-9cd5c5b241a9

📥 Commits

Reviewing files that changed from the base of the PR and between de6d464 and a5fa0eb.

📒 Files selected for processing (3)
  • tests/cypress/support/commands.ts
  • tests/cypress/support/plex-settings.commands.ts
  • tests/cypress/support/request.commands.ts

@jrh3k5 jrh3k5 force-pushed the user-unknown-error branch 3 times, most recently from de6d464 to a4f70e3 Compare March 22, 2026 00:20
@jrh3k5 jrh3k5 force-pushed the user-unknown-error branch from a4f70e3 to 452e7d6 Compare March 22, 2026 00:22
@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Mar 22, 2026

@tidusjar - have to confess, the test failures have me stumped. They seem pretty isolated to my branch, but nothing in my changes seems to alter the authentication scheme in a way that would regress the service in a way that seems to be happening.

Further, when I try to reproduce the error manually - e.g., the setup failure for in beforeach for "Load Servers from Plex.TV Api and Save", I can't reproduce the issue:

curl -X POST -v http://localhost:3577/api/v1/Settings/Plex/ \
  -H "Connection: keep-alive" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <JWT harvested from browser's network traffic>" \
  -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/145.0.0.0 Safari/537.36" \
  -H "Accept: */*" \
  -H "Accept-Encoding: gzip, deflate" \
  -H "Content-Length: 136" \
  -d '{"enable":false,"enableWatchlistImport":false,"monitorAll":false,"installId":"0c5c597d-56ea-4f34-8f59-18d34ec82482","servers":[],"id":2}'

Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:3577 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3577...
* Connected to localhost (::1) port 3577
> POST /api/v1/Settings/Plex/ HTTP/1.1
> Host: localhost:3577
> Connection: keep-alive
> Accept: application/json
> Content-Type: application/json
> Authorization: Bearer <snip>
> User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/145.0.0.0 Safari/537.36
> Accept: */*
> Accept-Encoding: gzip, deflate
> Content-Length: 136
> 
* upload completely sent off: 136 bytes
< HTTP/1.1 200 OK
< Content-Length: 4
< Content-Type: application/json; charset=utf-8
< Date: Sat, 21 Mar 2026 23:55:28 GMT
< Server: Kestrel
< 
* Connection #0 to host localhost left intact
true%

I'm wondering if I've accidentally exposed a latent issue in how the tests manage and supply JWTs? I tried to Copilot my way through the issue (I'm not at all familiar with Cypress), but wasn't able to produce anything fruitful.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants