From 8fc2685840c3c4cafab8e6039cba82563105de64 Mon Sep 17 00:00:00 2001 From: Stef Kariotidis Date: Sat, 29 Aug 2026 23:06:47 +0300 Subject: [PATCH 1/4] WIP: prove peer visibility uses POST (checkpoint; tests failing) --- .../Services/ProductionGroupsServiceTests.cs | 67 +++++++++++++++++++ .../WayfarerMobile.Tests.csproj | 1 + 2 files changed, 68 insertions(+) create mode 100644 tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs diff --git a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs new file mode 100644 index 0000000..3131f7d --- /dev/null +++ b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs @@ -0,0 +1,67 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Moq.Protected; +using WayfarerMobile.Services; + +namespace WayfarerMobile.Tests.Unit.Services; + +/// +/// Regression tests for the production peer-visibility request. +/// +public class ProductionGroupsServiceTests +{ + [Fact] + public async Task UpdatePeerVisibilityAsync_SendsAuthenticatedPostWithDisabledState() + { + var groupId = Guid.Parse("11111111-2222-3333-4444-555555555555"); + using var cancellationSource = new CancellationTokenSource(); + var cancellationToken = cancellationSource.Token; + HttpMethod? capturedMethod = null; + Uri? capturedUri = null; + string? capturedAuthorization = null; + string? capturedContent = null; + CancellationToken capturedCancellationToken = default; + var contactCount = 0; + var handler = new Mock(); + handler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback(async (request, token) => + { + contactCount++; + capturedMethod = request.Method; + capturedUri = request.RequestUri; + capturedAuthorization = request.Headers.Authorization?.ToString(); + capturedContent = await request.Content!.ReadAsStringAsync(token); + capturedCancellationToken = token; + }) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + using var httpClient = new HttpClient(handler.Object); + var httpClientFactory = new Mock(); + httpClientFactory.Setup(factory => factory.CreateClient("WayfarerApi")).Returns(httpClient); + var settings = new Mock(); + settings.Setup(value => value.IsConfigured).Returns(true); + settings.Setup(value => value.ServerUrl).Returns("https://api.example.com"); + settings.Setup(value => value.ApiToken).Returns("test-token-123"); + var service = new GroupsService( + settings.Object, + Mock.Of>(), + httpClientFactory.Object); + + var result = await service.UpdatePeerVisibilityAsync(groupId, disabled: true, cancellationToken); + + result.Should().BeTrue(); + contactCount.Should().Be(1); + capturedMethod.Should().Be(HttpMethod.Post); + capturedUri.Should().Be(new Uri($"https://api.example.com/api/mobile/groups/{groupId}/peer-visibility")); + capturedAuthorization.Should().Be("Bearer test-token-123"); + capturedCancellationToken.Should().Be(cancellationToken); + using var content = JsonDocument.Parse(capturedContent!); + content.RootElement.GetProperty("disabled").GetBoolean().Should().BeTrue(); + } +} diff --git a/tests/WayfarerMobile.Tests/WayfarerMobile.Tests.csproj b/tests/WayfarerMobile.Tests/WayfarerMobile.Tests.csproj index 2629738..cb7fef8 100644 --- a/tests/WayfarerMobile.Tests/WayfarerMobile.Tests.csproj +++ b/tests/WayfarerMobile.Tests/WayfarerMobile.Tests.csproj @@ -25,6 +25,7 @@ + From e78462b955c8455410e57a7eba208b6fd31b81bd Mon Sep 17 00:00:00 2001 From: Stef Kariotidis Date: Sat, 29 Aug 2026 23:10:07 +0300 Subject: [PATCH 2/4] Fix peer visibility update method --- .../Interfaces/IGroupsService.cs | 2 +- .../Models/GroupLocationModels.cs | 2 +- src/WayfarerMobile/Services/GroupsService.cs | 2 +- .../Services/ProductionGroupsServiceTests.cs | 62 +++++++++++++++---- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/WayfarerMobile.Core/Interfaces/IGroupsService.cs b/src/WayfarerMobile.Core/Interfaces/IGroupsService.cs index 716619d..0ee7339 100644 --- a/src/WayfarerMobile.Core/Interfaces/IGroupsService.cs +++ b/src/WayfarerMobile.Core/Interfaces/IGroupsService.cs @@ -49,7 +49,7 @@ Task> GetLatestLocationsAsync( /// /// Updates the current user's peer visibility setting. - /// PATCH /api/mobile/groups/{groupId}/peer-visibility + /// POST /api/mobile/groups/{groupId}/peer-visibility /// /// The group ID. /// Whether peer visibility should be disabled. diff --git a/src/WayfarerMobile.Core/Models/GroupLocationModels.cs b/src/WayfarerMobile.Core/Models/GroupLocationModels.cs index d691684..2e91eec 100644 --- a/src/WayfarerMobile.Core/Models/GroupLocationModels.cs +++ b/src/WayfarerMobile.Core/Models/GroupLocationModels.cs @@ -135,7 +135,7 @@ public class GroupLocationResult /// /// Request to update peer visibility. -/// PATCH /api/mobile/groups/{groupId}/peer-visibility +/// POST /api/mobile/groups/{groupId}/peer-visibility /// public class PeerVisibilityUpdateRequest { diff --git a/src/WayfarerMobile/Services/GroupsService.cs b/src/WayfarerMobile/Services/GroupsService.cs index d8a71ee..162fb26 100644 --- a/src/WayfarerMobile/Services/GroupsService.cs +++ b/src/WayfarerMobile/Services/GroupsService.cs @@ -287,7 +287,7 @@ public async Task UpdatePeerVisibilityAsync( try { - var request = CreateRequest(new HttpMethod("PATCH"), $"/api/mobile/groups/{groupId}/peer-visibility"); + var request = CreateRequest(HttpMethod.Post, $"/api/mobile/groups/{groupId}/peer-visibility"); request.Content = JsonContent.Create(new PeerVisibilityUpdateRequest { Disabled = disabled }, options: JsonOptions); var response = await HttpClientInstance.SendAsync(request, cancellationToken); diff --git a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs index 3131f7d..54e9703 100644 --- a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs +++ b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs @@ -11,17 +11,19 @@ namespace WayfarerMobile.Tests.Unit.Services; /// public class ProductionGroupsServiceTests { - [Fact] - public async Task UpdatePeerVisibilityAsync_SendsAuthenticatedPostWithDisabledState() + [Theory] + [InlineData(true, HttpStatusCode.OK, true)] + [InlineData(false, HttpStatusCode.Forbidden, false)] + public async Task UpdatePeerVisibilityAsync_SendsOneAuthenticatedPostAndReturnsStatusResult( + bool disabled, + HttpStatusCode statusCode, + bool expectedResult) { var groupId = Guid.Parse("11111111-2222-3333-4444-555555555555"); - using var cancellationSource = new CancellationTokenSource(); - var cancellationToken = cancellationSource.Token; HttpMethod? capturedMethod = null; Uri? capturedUri = null; string? capturedAuthorization = null; string? capturedContent = null; - CancellationToken capturedCancellationToken = default; var contactCount = 0; var handler = new Mock(); handler @@ -37,9 +39,8 @@ public async Task UpdatePeerVisibilityAsync_SendsAuthenticatedPostWithDisabledSt capturedUri = request.RequestUri; capturedAuthorization = request.Headers.Authorization?.ToString(); capturedContent = await request.Content!.ReadAsStringAsync(token); - capturedCancellationToken = token; }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + .ReturnsAsync(new HttpResponseMessage(statusCode)); using var httpClient = new HttpClient(handler.Object); var httpClientFactory = new Mock(); @@ -53,15 +54,54 @@ public async Task UpdatePeerVisibilityAsync_SendsAuthenticatedPostWithDisabledSt Mock.Of>(), httpClientFactory.Object); - var result = await service.UpdatePeerVisibilityAsync(groupId, disabled: true, cancellationToken); + var result = await service.UpdatePeerVisibilityAsync(groupId, disabled); - result.Should().BeTrue(); + result.Should().Be(expectedResult); contactCount.Should().Be(1); capturedMethod.Should().Be(HttpMethod.Post); capturedUri.Should().Be(new Uri($"https://api.example.com/api/mobile/groups/{groupId}/peer-visibility")); capturedAuthorization.Should().Be("Bearer test-token-123"); - capturedCancellationToken.Should().Be(cancellationToken); using var content = JsonDocument.Parse(capturedContent!); - content.RootElement.GetProperty("disabled").GetBoolean().Should().BeTrue(); + content.RootElement.GetProperty("disabled").GetBoolean().Should().Be(disabled); + } + + [Fact] + public async Task UpdatePeerVisibilityAsync_ForwardsCancellationToSendAsync() + { + var sendStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var handlerCancellationObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var handler = new Mock(); + handler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Returns(async (_, token) => + { + using var registration = token.Register(() => handlerCancellationObserved.TrySetResult()); + sendStarted.SetResult(); + await Task.Delay(Timeout.InfiniteTimeSpan, token); + return new HttpResponseMessage(HttpStatusCode.OK); + }); + + using var httpClient = new HttpClient(handler.Object); + var httpClientFactory = new Mock(); + httpClientFactory.Setup(factory => factory.CreateClient("WayfarerApi")).Returns(httpClient); + var settings = new Mock(); + settings.Setup(value => value.IsConfigured).Returns(true); + settings.Setup(value => value.ServerUrl).Returns("https://api.example.com"); + var service = new GroupsService( + settings.Object, + Mock.Of>(), + httpClientFactory.Object); + using var cancellationSource = new CancellationTokenSource(); + + var resultTask = service.UpdatePeerVisibilityAsync(Guid.NewGuid(), disabled: true, cancellationSource.Token); + await sendStarted.Task; + cancellationSource.Cancel(); + + await handlerCancellationObserved.Task.WaitAsync(TimeSpan.FromSeconds(1)); + (await resultTask).Should().BeFalse(); } } From 040c13b44891a6a6ed0fee3af24df4f5c4f4fb67 Mon Sep 17 00:00:00 2001 From: Stef Kariotidis Date: Sat, 29 Aug 2026 23:22:36 +0300 Subject: [PATCH 3/4] Make peer visibility request capture deterministic --- .../Unit/Services/ProductionGroupsServiceTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs index 54e9703..24d55d7 100644 --- a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs +++ b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs @@ -32,15 +32,16 @@ public async Task UpdatePeerVisibilityAsync_SendsOneAuthenticatedPostAndReturnsS "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback(async (request, token) => + .Returns(async (request, token) => { contactCount++; capturedMethod = request.Method; capturedUri = request.RequestUri; capturedAuthorization = request.Headers.Authorization?.ToString(); capturedContent = await request.Content!.ReadAsStringAsync(token); - }) - .ReturnsAsync(new HttpResponseMessage(statusCode)); + + return new HttpResponseMessage(statusCode); + }); using var httpClient = new HttpClient(handler.Object); var httpClientFactory = new Mock(); From 9fb9da813431358e6ecc549f1f1f97921a517d76 Mon Sep 17 00:00:00 2001 From: Stef Kariotidis Date: Sat, 29 Aug 2026 23:45:26 +0300 Subject: [PATCH 4/4] Make cancellation propagation test deterministic --- .../Unit/Services/ProductionGroupsServiceTests.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs index 24d55d7..474bd3c 100644 --- a/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs +++ b/tests/WayfarerMobile.Tests/Unit/Services/ProductionGroupsServiceTests.cs @@ -70,7 +70,9 @@ public async Task UpdatePeerVisibilityAsync_SendsOneAuthenticatedPostAndReturnsS public async Task UpdatePeerVisibilityAsync_ForwardsCancellationToSendAsync() { var sendStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var handlerCancellationObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var sendCompletion = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + var capturedCancellationToken = CancellationToken.None; var handler = new Mock(); handler .Protected() @@ -78,12 +80,11 @@ public async Task UpdatePeerVisibilityAsync_ForwardsCancellationToSendAsync() "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Returns(async (_, token) => + .Returns((_, token) => { - using var registration = token.Register(() => handlerCancellationObserved.TrySetResult()); + capturedCancellationToken = token; sendStarted.SetResult(); - await Task.Delay(Timeout.InfiniteTimeSpan, token); - return new HttpResponseMessage(HttpStatusCode.OK); + return sendCompletion.Task; }); using var httpClient = new HttpClient(handler.Object); @@ -102,7 +103,8 @@ public async Task UpdatePeerVisibilityAsync_ForwardsCancellationToSendAsync() await sendStarted.Task; cancellationSource.Cancel(); - await handlerCancellationObserved.Task.WaitAsync(TimeSpan.FromSeconds(1)); + capturedCancellationToken.IsCancellationRequested.Should().BeTrue(); + sendCompletion.SetCanceled(capturedCancellationToken); (await resultTask).Should().BeFalse(); } }