Skip to content

Test: SignalR hub and realtime integration tests (#706)#751

Merged
Chris0Jeky merged 6 commits intomainfrom
test/706-signalr-hub-integration
Apr 4, 2026
Merged

Test: SignalR hub and realtime integration tests (#706)#751
Chris0Jeky merged 6 commits intomainfrom
test/706-signalr-hub-integration

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

  • 19 integration tests for BoardsHub SignalR hub covering presence lifecycle, authentication enforcement, multi-user collaboration, and edge cases
  • Uses WebApplicationFactory with real SignalR pipeline (in-memory transport, no network)
  • Custom EventCollector<T> helper provides insertion-ordered, thread-safe event collection to avoid race conditions in assertions
  • Also fixes a pre-existing build error in AuthControllerEdgeCaseTests (missing IUserContext parameter)

Test coverage

  • Authentication (3): unauthenticated reject, valid token connect, invalid token reject
  • Presence lifecycle (5): join broadcast, set editing card, clear editing card, leave (via observer), abrupt disconnect cleanup
  • Multi-user (2): multiple users see all members, same-user two-connection aggregation
  • Authorization (3): join without access, leave without access, edit without join
  • Edge cases (6): board switching, two-tab disconnect-one, non-existent board, Guid.Empty sanitization, timestamp, cross-board isolation

Closes #706

Test plan

  • All 19 new tests pass: dotnet test --filter "FullyQualifiedName~BoardsHubIntegrationTests"
  • Existing InMemoryBoardPresenceTrackerTests still pass
  • CI pipeline passes

AuthController constructor added IUserContext parameter in a prior PR
but the unit tests were not updated. Pass a mock IUserContext to fix
the compilation error.
Provides CreateBoardsHubConnection helper using WebApplicationFactory's
in-memory test server, and an insertion-ordered EventCollector for
deterministic event assertion without race conditions.
Covers presence lifecycle (join, leave, editing state, abrupt disconnect),
authentication enforcement (unauthenticated, invalid token, no board access,
join-before-edit guard), multi-user presence, same-user multi-connection
aggregation, board switching, Guid.Empty sanitization, timestamp validity,
and group isolation (no cross-board event leakage).

Uses real SignalR pipeline via WebApplicationFactory test server.

Closes #706
Copilot AI review requested due to automatic review settings April 4, 2026 01:14
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial Self-Review

Potential issues found

  1. Unused import in SignalRTestHelper.cs: System.Collections.Concurrent is imported but not used (switched from ConcurrentBag to custom EventCollector). Cosmetic, not functional.

  2. GrantAccessDto response not asserted in multi-user tests: The PostAsJsonAsync calls to grant board access don't check the response status code in several tests (lines 171-173, 212-214, 255-257). If granting access fails silently, subsequent hub operations would fail with confusing "Forbidden" errors rather than a clear setup failure.

  3. Connection_ShouldNotReceiveEventsFromUnsubscribedBoard uses Task.Delay(500): This is a negative test (proving something does NOT happen). The 500ms delay is a reasonable pragmatic choice for in-memory transport, but could theoretically be flaky on extremely slow CI. Acceptable tradeoff.

  4. No boardMutation event tests: The issue asked for board mutation event delivery tests (card created/moved/updated broadcast). The current tests only cover boardPresence events. The SignalRBoardRealtimeNotifier sends boardMutation events via IHubContext<BoardsHub> which goes through a different code path (service-level broadcast, not hub method invocation). This is a gap but requires more complex test setup (creating cards while connected, then verifying the mutation event arrives).

  5. No toolStatus event tests: Similarly, the issue mentioned SignalRToolStatusNotifier tests. These require triggering a tool-calling chat session and verifying toolStatus events arrive, which involves the mock LLM provider and chat service orchestration. This would significantly increase test scope.

  6. Flakiness risk in AbruptDisconnect: The test relies on DisposeAsync() triggering OnDisconnectedAsync promptly. In the test server, this is synchronous-enough, but if OnDisconnectedAsync were ever made truly async with delays, this could become flaky. Current implementation is fine.

  7. Resource leak: conn2 in AbruptDisconnect is not in await using: Line 225 creates conn2 without await using because we need to manually dispose it mid-test. This is intentional and correct — DisposeAsync() IS called explicitly. But worth noting.

Assessment

  • Tests are meaningful (not false-positive) — they exercise the real SignalR pipeline end-to-end
  • Assertions verify specific field values, not just "no exception thrown"
  • Thread-safe EventCollector prevents ConcurrentBag ordering issues
  • Main gap: boardMutation and toolStatus event delivery tests are not covered

Recommendation

Items 2 (missing status assertions on grant access) and 1 (unused import) should be fixed. Items 4-5 (mutation/tool events) could be tracked as follow-up.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces comprehensive integration tests for the BoardsHub SignalR hub, covering authentication, presence lifecycle, and authorization enforcement. It also updates AuthController tests to include a mocked IUserContext and provides a new SignalRTestHelper utility to facilitate hub testing. The review feedback focuses on improving test reliability and code quality by asserting specific exception types instead of the base Exception, utilizing 'await using' for guaranteed connection disposal, avoiding potentially flaky Task.Delay calls, and maintaining consistency with the project's preference for DateTimeOffset.

await using var connection = SignalRTestHelper.CreateBoardsHubConnection(_factory, accessToken: null);

var act = () => connection.StartAsync();
await act.Should().ThrowAsync<Exception>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Catching the base Exception type is too broad for an authentication test. It could mask other issues like configuration errors or null references. It is better to assert that a specific exception related to authentication failure is thrown (e.g., HttpRequestException or a specific SignalR exception wrapping the 401 response).

await using var connection = SignalRTestHelper.CreateBoardsHubConnection(_factory, "not-a-valid-jwt");

var act = () => connection.StartAsync();
await act.Should().ThrowAsync<Exception>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similar to the unauthenticated connection test, catching a generic Exception here is discouraged. Specifying the expected exception type ensures the test fails if the connection fails for an unexpected reason.

await SignalRTestHelper.WaitForEventsAsync(events1, 1);

// User2 connects and joins
var conn2 = SignalRTestHelper.CreateBoardsHubConnection(_factory, user2.Token);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using await using for the connection is safer. If an assertion fails before the manual DisposeAsync() call at line 237, the connection might not be disposed promptly. HubConnection.DisposeAsync() is idempotent, so calling it manually to simulate a disconnect is still valid even with await using.

        await using var conn2 = SignalRTestHelper.CreateBoardsHubConnection(_factory, user2.Token);

await conn1.InvokeAsync("JoinBoard", board.Id);
await SignalRTestHelper.WaitForEventsAsync(events1, 1);

var conn2 = SignalRTestHelper.CreateBoardsHubConnection(_factory, user.Token);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using await using for the connection is safer here as well, ensuring disposal if an assertion fails before the manual DisposeAsync() call at line 417.

        await using var conn2 = SignalRTestHelper.CreateBoardsHubConnection(_factory, user.Token);

await conn2.InvokeAsync("JoinBoard", boardB.Id);

// Give events time to arrive (they shouldn't)
await Task.Delay(500);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using a fixed Task.Delay to assert that an event is not received can lead to flaky tests, especially in CI environments where execution might be slower. While testing the absence of an event is challenging, consider if there's a more deterministic way to verify isolation, or at least use a named constant for the delay to clarify its purpose.

Comment on lines +1 to +2
using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR.Client;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The System.Collections.Concurrent namespace is not used in this file. Removing unused directives keeps the code clean.

using Microsoft.AspNetCore.SignalR.Client;

Comment on lines +46 to +47
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
while (collector.Count < expectedCount && DateTime.UtcNow < deadline)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The project generally prefers DateTimeOffset.UtcNow for absolute timestamps. Using DateTime.UtcNow here is inconsistent with other parts of the codebase and can lead to issues when comparing mixed types.

        var deadline = DateTimeOffset.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
        while (collector.Count < expectedCount && DateTimeOffset.UtcNow < deadline)

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

Adds backend integration coverage for the BoardsHub SignalR realtime presence flow using the in-memory WebApplicationFactory pipeline, and updates an existing auth controller edge-case test to match the current controller constructor.

Changes:

  • Introduces BoardsHubIntegrationTests exercising hub authentication, presence lifecycle, authorization failures, and several edge cases.
  • Adds SignalRTestHelper + EventCollector<T> to build in-memory HubConnections and collect events for deterministic assertions.
  • Fixes AuthControllerEdgeCaseTests compilation by supplying the newly-required IUserContext constructor dependency.

Reviewed changes

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

File Description
backend/tests/Taskdeck.Api.Tests/Support/SignalRTestHelper.cs New helper for creating hub connections against the test server and collecting hub events.
backend/tests/Taskdeck.Api.Tests/BoardsHubIntegrationTests.cs New suite of integration tests covering BoardsHub presence/auth/authorization and edge cases.
backend/tests/Taskdeck.Api.Tests/AuthControllerEdgeCaseTests.cs Updates controller construction to include IUserContext, fixing a pre-existing build break.

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

@@ -0,0 +1,83 @@
using System.Collections.Concurrent;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Remove the unused using System.Collections.Concurrent; (it isn't referenced in this file), to avoid analyzer/warning noise in the test project.

Suggested change
using System.Collections.Concurrent;

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +52
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
while (collector.Count < expectedCount && DateTime.UtcNow < deadline)
{
await Task.Delay(50);
}

return collector.ToList();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

WaitForEventsAsync returns the collected events even when the timeout expires without reaching expectedCount, which makes timeouts harder to diagnose and can lead to unclear failures downstream. Consider throwing a TimeoutException (including expected vs actual counts) when the deadline is reached without enough events.

Suggested change
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
while (collector.Count < expectedCount && DateTime.UtcNow < deadline)
{
await Task.Delay(50);
}
return collector.ToList();
var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(5);
var deadline = DateTime.UtcNow + effectiveTimeout;
while (collector.Count < expectedCount && DateTime.UtcNow < deadline)
{
await Task.Delay(50);
}
var events = collector.ToList();
if (events.Count < expectedCount)
{
throw new System.TimeoutException(
$"Timed out waiting for {expectedCount} event(s). Received {events.Count} event(s) within {effectiveTimeout}.");
}
return events;

Copilot uses AI. Check for mistakes.

/// <summary>
/// Integration tests for the BoardsHub SignalR hub covering presence lifecycle,
/// authentication enforcement, board mutation event delivery, and edge cases.
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The XML summary says these tests cover "board mutation event delivery", but this test class only subscribes/asserts on boardPresence snapshots. Either add mutation-delivery tests (e.g., card/column events) or adjust the comment to match the actual coverage.

Suggested change
/// authentication enforcement, board mutation event delivery, and edge cases.
/// authentication enforcement, boardPresence snapshot delivery, and edge cases.

Copilot uses AI. Check for mistakes.
await using var connection = SignalRTestHelper.CreateBoardsHubConnection(_factory, accessToken: null);

var act = () => connection.StartAsync();
await act.Should().ThrowAsync<Exception>();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

This assertion will pass for any exception type/cause (e.g., server error, routing misconfig), not specifically an authentication rejection. Tighten the assertion to validate the failure is due to auth (e.g., assert the negotiate/start exception indicates 401/Unauthorized).

Suggested change
await act.Should().ThrowAsync<Exception>();
var exception = await act.Should().ThrowAsync<Exception>();
exception.Which.ToString().Should().MatchRegex("(?i)(401|Unauthorized)");

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +60
public async Task InvalidToken_Connection_ShouldFailToStart()
{
await using var connection = SignalRTestHelper.CreateBoardsHubConnection(_factory, "not-a-valid-jwt");

var act = () => connection.StartAsync();
await act.Should().ThrowAsync<Exception>();
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Like the unauthenticated case, asserting a generic Exception here can mask failures unrelated to token validation. Consider asserting the error indicates an auth failure (401/Unauthorized) so the test doesn't pass on unrelated connection-start errors.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +214
await client1.PostAsJsonAsync(
$"/api/boards/{board.Id}/access",
new GrantAccessDto(board.Id, user2.UserId, UserRole.Editor));
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The response from the access grant request isn't validated. Add a status-code assertion (or EnsureSuccessStatusCode) so authorization setup failures are caught at the right place.

Suggested change
await client1.PostAsJsonAsync(
$"/api/boards/{board.Id}/access",
new GrantAccessDto(board.Id, user2.UserId, UserRole.Editor));
var grantAccessResponse = await client1.PostAsJsonAsync(
$"/api/boards/{board.Id}/access",
new GrantAccessDto(board.Id, user2.UserId, UserRole.Editor));
grantAccessResponse.EnsureSuccessStatusCode();

Copilot uses AI. Check for mistakes.
Comment on lines +255 to +257
await client1.PostAsJsonAsync(
$"/api/boards/{board.Id}/access",
new GrantAccessDto(board.Id, user2.UserId, UserRole.Editor));
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The response from the access grant request isn't checked. Consider asserting success here to avoid confusing failures later when the second user tries to join the board.

Suggested change
await client1.PostAsJsonAsync(
$"/api/boards/{board.Id}/access",
new GrantAccessDto(board.Id, user2.UserId, UserRole.Editor));
using var grantAccessResponse = await client1.PostAsJsonAsync(
$"/api/boards/{board.Id}/access",
new GrantAccessDto(board.Id, user2.UserId, UserRole.Editor));
grantAccessResponse.EnsureSuccessStatusCode();

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +137
new CreateColumnDto(board.Id, "Backlog", null, null));
var column = await colResponse.Content.ReadFromJsonAsync<ColumnDto>();
var cardResponse = await client.PostAsJsonAsync(
$"/api/boards/{board.Id}/cards",
new CreateCardDto(board.Id, column!.Id, "Test Card", null, null, null));
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

colResponse isn't validated before reading/deserializing the body. Add a status-code assertion (e.g., Created) or EnsureSuccessStatusCode() to make failures easier to diagnose.

Suggested change
new CreateColumnDto(board.Id, "Backlog", null, null));
var column = await colResponse.Content.ReadFromJsonAsync<ColumnDto>();
var cardResponse = await client.PostAsJsonAsync(
$"/api/boards/{board.Id}/cards",
new CreateCardDto(board.Id, column!.Id, "Test Card", null, null, null));
new CreateColumnDto(board.Id, "Backlog", null, null));
colResponse.StatusCode.Should().Be(HttpStatusCode.Created);
var column = await colResponse.Content.ReadFromJsonAsync<ColumnDto>();
var cardResponse = await client.PostAsJsonAsync(
$"/api/boards/{board.Id}/cards",
new CreateCardDto(board.Id, column!.Id, "Test Card", null, null, null));
cardResponse.StatusCode.Should().Be(HttpStatusCode.Created);

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +137
new CreateColumnDto(board.Id, "Backlog", null, null));
var column = await colResponse.Content.ReadFromJsonAsync<ColumnDto>();
var cardResponse = await client.PostAsJsonAsync(
$"/api/boards/{board.Id}/cards",
new CreateCardDto(board.Id, column!.Id, "Test Card", null, null, null));
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

cardResponse isn't validated before deserializing. As with other tests in this file, assert the expected status code (e.g., Created) before using the response content.

Suggested change
new CreateColumnDto(board.Id, "Backlog", null, null));
var column = await colResponse.Content.ReadFromJsonAsync<ColumnDto>();
var cardResponse = await client.PostAsJsonAsync(
$"/api/boards/{board.Id}/cards",
new CreateCardDto(board.Id, column!.Id, "Test Card", null, null, null));
new CreateColumnDto(board.Id, "Backlog", null, null));
colResponse.StatusCode.Should().Be(HttpStatusCode.Created);
var column = await colResponse.Content.ReadFromJsonAsync<ColumnDto>();
var cardResponse = await client.PostAsJsonAsync(
$"/api/boards/{board.Id}/cards",
new CreateCardDto(board.Id, column!.Id, "Test Card", null, null, null));
cardResponse.StatusCode.Should().Be(HttpStatusCode.Created);

Copilot uses AI. Check for mistakes.
Comment on lines +504 to +511
// Give events time to arrive (they shouldn't)
await Task.Delay(500);

// Only the initial join event from board A should be present
var collected = events.ToList();
collected.Should().HaveCount(1);
collected.Single().BoardId.Should().Be(boardA.Id);
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

This test relies on a fixed Task.Delay(500) and then checks the current list, which can create false positives (an unexpected event could arrive just after the snapshot is taken). Prefer an assertion helper that actively fails if any additional event arrives within a defined window.

Suggested change
// Give events time to arrive (they shouldn't)
await Task.Delay(500);
// Only the initial join event from board A should be present
var collected = events.ToList();
collected.Should().HaveCount(1);
collected.Single().BoardId.Should().Be(boardA.Id);
}
// Assert that no additional events arrive within the observation window.
var collected = await AssertNoAdditionalEventsAsync(events, expectedCount: 1, TimeSpan.FromMilliseconds(500));
// Only the initial join event from board A should be present
collected.Should().HaveCount(1);
collected.Single().BoardId.Should().Be(boardA.Id);
}
private static async Task<IReadOnlyList<BoardPresenceSnapshot>> AssertNoAdditionalEventsAsync(
EventCollector<BoardPresenceSnapshot> events,
int expectedCount,
TimeSpan observationWindow)
{
var deadline = DateTimeOffset.UtcNow + observationWindow;
while (DateTimeOffset.UtcNow < deadline)
{
var snapshot = events.ToList();
snapshot.Should().HaveCount(expectedCount, "no additional boardPresence events should arrive during the observation window");
await Task.Delay(50);
}
var finalSnapshot = events.ToList();
finalSnapshot.Should().HaveCount(expectedCount, "no additional boardPresence events should arrive during the observation window");
return finalSnapshot;
}

Copilot uses AI. Check for mistakes.
… import

- Add StatusCode assertions on all GrantAccessDto calls so test setup
  failures surface clearly instead of causing confusing Forbidden errors
- Remove unused System.Collections.Concurrent import from SignalRTestHelper
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Review fixes applied (304af98)

Fixed items from adversarial review:

  1. Unused import removed from SignalRTestHelper.cs
  2. Grant-access precondition assertions added to all 3 multi-user tests (LeaveBoard_ShouldRemoveUserFromPresence, AbruptDisconnect_ShouldCleanUpPresence, MultipleUsersOnSameBoard_ShouldSeeAllMembersInPresence) — test setup failures now surface immediately with clear messages

Remaining known gaps (not blocking, can be follow-up):

  • boardMutation event delivery tests (requires card CRUD while connected)
  • toolStatus event delivery tests (requires mock LLM tool-calling orchestration)

- Replace generic Exception assertions with HttpRequestException + 401
  check in auth tests (prevents false positives from config errors)
- WaitForEventsAsync now throws TimeoutException when expected events
  do not arrive, making timeout failures immediately diagnosable
- Use DateTimeOffset.UtcNow instead of DateTime.UtcNow for consistency
- Add missing status code assertions on column/card creation in
  ClearEditingCard test
- Fix XML doc to not claim boardMutation coverage that does not exist
- Use await using for conn2 in DisconnectOne test to prevent resource
  leak on assertion failure
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Second-Pass Adversarial Review

Issues found and fixed (474fefd)

1. False-positive auth tests (HIGH)
Unauthenticated_Connection_ShouldFailToStart and InvalidToken_Connection_ShouldFailToStart caught bare Exception. This means they would pass if the connection failed for any reason (routing misconfiguration, server startup error, null reference) -- not just authentication rejection. Fixed to assert HttpRequestException with 401/Unauthorized status.

2. Silent timeout in WaitForEventsAsync (HIGH)
When events did not arrive within the deadline, WaitForEventsAsync silently returned whatever was collected. Downstream assertions then ran on insufficient data, producing misleading failure messages (e.g., "expected 2 members but found 0" instead of "timed out waiting for events"). Fixed to throw TimeoutException with expected vs. actual counts.

3. DateTime.UtcNow instead of DateTimeOffset.UtcNow (LOW)
WaitForEventsAsync used DateTime.UtcNow while the rest of the codebase (including BoardPresenceSnapshot.OccurredAt) uses DateTimeOffset.UtcNow. Fixed for consistency.

4. Missing precondition assertions in ClearEditingCard (MEDIUM)
Column and card creation responses were not checked for success status codes. If these setup steps failed, the test would produce a confusing NullReferenceException downstream rather than a clear "setup failed" signal. Fixed by adding HttpStatusCode.Created assertions.

5. Misleading XML doc comment (LOW)
The class doc claimed coverage of "board mutation event delivery" but no boardMutation event tests exist. Fixed to accurately describe what is tested.

6. Resource leak risk in DisconnectOne test (LOW)
conn2 was not await using, so if an assertion failed before the explicit DisposeAsync() call, the connection would leak. Fixed to use await using (idempotent dispose is safe).

Items reviewed but not changed

  • Task.Delay(500) in negative test: The ShouldNotReceiveEventsFromUnsubscribedBoard test uses a 500ms delay to prove events do NOT arrive. This is an inherent limitation of negative tests. For in-memory transport, 500ms is generous. Acceptable tradeoff.
  • conn2 in AbruptDisconnect not await using: This is intentional -- the test needs explicit control over when dispose happens to simulate abrupt disconnect. Since the explicit DisposeAsync() is called before assertions, the resource leak risk is minimal.
  • Missing boardMutation/toolStatus event tests: These are acknowledged gaps from the first review and tracked as follow-up work, not blocking for this PR.

@Chris0Jeky Chris0Jeky merged commit 387c17c into main Apr 4, 2026
23 checks passed
@github-project-automation github-project-automation bot moved this from Pending to Done in Taskdeck Execution Apr 4, 2026
@Chris0Jeky Chris0Jeky deleted the test/706-signalr-hub-integration branch April 4, 2026 02:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

TST-39: SignalR hub and realtime integration tests

2 participants