diff --git a/src/Core/Services/Implementations/RelayPushNotificationService.cs b/src/Core/Services/Implementations/RelayPushNotificationService.cs index 9d548440b..bbd56992a 100644 --- a/src/Core/Services/Implementations/RelayPushNotificationService.cs +++ b/src/Core/Services/Implementations/RelayPushNotificationService.cs @@ -242,6 +242,11 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti await SendAsync(HttpMethod.Post, "push/send", request); } + internal virtual async Task SendAsync(HttpMethod method, string path, object payload) + { + await base.SendAsync(method, path, payload); + } + private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier) { var currentContext = diff --git a/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs index 4f6929aa9..3cc9f5091 100644 --- a/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs @@ -20,7 +20,7 @@ using Xunit; namespace Bit.Core.Test.Services; -[SutProviderCustomize] +[SutProviderCustomize(false)] [HttpClientCustomize] public class NotificationsApiPushNotificationServiceTests { @@ -32,9 +32,10 @@ public class NotificationsApiPushNotificationServiceTests MockedHttpMessageHandler mockedHttpMessageHandler) { globalSettings.SelfHosted = true; - globalSettings.BaseServiceUri = new GlobalSettings.BaseServiceUriSettings(globalSettings); globalSettings.ProjectName = "Notifications"; globalSettings.InternalIdentityKey = "internal-identity-key"; + sutProvider.SetDependency(typeof(GlobalSettings), globalSettings) + .Create(); var tokenResponse = mockedHttpMessageHandler .When(request => @@ -59,12 +60,6 @@ public class NotificationsApiPushNotificationServiceTests request.Headers.Authorization?.ToString() == "Bearer token") .RespondWith(HttpStatusCode.OK); - sutProvider.Reset(); - sutProvider.SetDependency(typeof(GlobalSettings), globalSettings) - .Create(); - - // var sut = new NotificationsApiPushNotificationService(httpFactory, globalSettings, httpContextAccessor, logger); - await sutProvider.Sut.SendAsync(HttpMethod.Post, "send", "payload"); Assert.Equal(1, tokenResponse.NumberOfResponses); @@ -79,6 +74,11 @@ public class NotificationsApiPushNotificationServiceTests SutProvider sutProvider, Notification notification, Guid deviceIdentifier, ICurrentContext currentContext, MockedHttpMessageHandler mockedHttpMessageHandler) { + sutProvider.Create(); + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + var tokenResponse = mockedHttpMessageHandler .When(request => request.Method == HttpMethod.Post && request.RequestUri!.ToString().EndsWith("/connect/token")) @@ -98,10 +98,6 @@ public class NotificationsApiPushNotificationServiceTests }) .RespondWith(HttpStatusCode.OK); - currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); - sutProvider.GetDependency().HttpContext!.RequestServices - .GetService(Arg.Any()).Returns(currentContext); - await sutProvider.Sut.PushSyncNotificationAsync(notification); Assert.Equal(1, tokenResponse.NumberOfResponses); diff --git a/test/Core.Test/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Services/RelayPushNotificationServiceTests.cs index ccf5e3d4b..688534e3a 100644 --- a/test/Core.Test/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Services/RelayPushNotificationServiceTests.cs @@ -1,45 +1,307 @@ -using Bit.Core.Repositories; +#nullable enable +using System.Net; +using System.Net.Http.Json; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.Models.Api; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Test.AutoFixture; +using Bit.Core.Test.AutoFixture.CurrentContextFixtures; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.MockedHttpClient; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.WebUtilities; using NSubstitute; using Xunit; namespace Bit.Core.Test.Services; +[SutProviderCustomize(false)] +[HttpClientCustomize] public class RelayPushNotificationServiceTests { - private readonly RelayPushNotificationService _sut; - - private readonly IHttpClientFactory _httpFactory; - private readonly IDeviceRepository _deviceRepository; - private readonly GlobalSettings _globalSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ILogger _logger; - - public RelayPushNotificationServiceTests() + [Theory] + [BitAutoData] + [GlobalSettingsCustomize] + public async void Constructor_DefaultGlobalSettings_CorrectHttpRequests( + SutProvider sutProvider, GlobalSettings globalSettings, + MockedHttpMessageHandler mockedHttpMessageHandler, Guid installationId) { - _httpFactory = Substitute.For(); - _deviceRepository = Substitute.For(); - _globalSettings = new GlobalSettings(); - _httpContextAccessor = Substitute.For(); - _logger = Substitute.For>(); + ConfigureSut(sutProvider, globalSettings, installationId); - _sut = new RelayPushNotificationService( - _httpFactory, - _deviceRepository, - _globalSettings, - _httpContextAccessor, - _logger - ); + var tokenResponse = mockedHttpMessageHandler + .When(request => + { + if (request.Method != HttpMethod.Post || + !request.RequestUri!.Equals(new Uri("http://installation-identity-test.localhost/connect/token"))) + { + return false; + } + + Assert.NotNull(request.Content); + Assert.NotNull(request.Content.Headers.ContentType); + Assert.Equal("application/x-www-form-urlencoded", request.Content.Headers.ContentType.MediaType); + + var formReader = new FormReader(request.Content.ReadAsStream()).ReadForm(); + + Assert.Contains("scope", formReader); + Assert.Equal("api.push", formReader["scope"]); + Assert.Contains("client_id", formReader); + Assert.Equal($"installation.{globalSettings.Installation.Id}", formReader["client_id"]); + Assert.Contains("client_secret", formReader); + Assert.Equal("installation-key", formReader["client_secret"]); + + return true; + }) + .RespondWith(HttpStatusCode.OK, new StringContent("{\"access_token\":\"token\"}")); + var sendResponse = mockedHttpMessageHandler + .When(request => + { + if (request.Method != HttpMethod.Post || + !request.RequestUri!.Equals(new Uri("http://push-relay-test.localhost/push/send"))) + { + return false; + } + + Assert.Equal("Bearer token", request.Headers.Authorization?.ToString()); + + return true; + }) + .RespondWith(HttpStatusCode.OK); + + await sutProvider.Sut.SendAsync(HttpMethod.Post, "push/send", "payload"); + + Assert.Equal(1, tokenResponse.NumberOfResponses); + Assert.Equal(1, sendResponse.NumberOfResponses); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + [Theory] + [BitAutoData] + [NotificationCustomize] + [CurrentContextCustomize] + [GlobalSettingsCustomize] + public async void PushSyncNotificationAsync_GlobalNotification_NothingSent( + SutProvider sutProvider, Notification notification, + MockedHttpMessageHandler mockedHttpMessageHandler, GlobalSettings globalSettings, Guid installationId) { - Assert.NotNull(_sut); + ConfigureSut(sutProvider, globalSettings, installationId); + + var response = mockedHttpMessageHandler + .When(request => true) + .RespondWith(HttpStatusCode.OK); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + Assert.Equal(0, response.NumberOfResponses); + } + + [Theory] + [BitAutoData(true, true, true)] + [BitAutoData(true, true, false)] + [BitAutoData(true, false, true)] + [BitAutoData(true, false, false)] + [BitAutoData(false, true, true)] + [BitAutoData(false, true, false)] + [BitAutoData(false, false, true)] + [BitAutoData(false, false, false)] + [NotificationCustomize(false)] + [CurrentContextCustomize] + [GlobalSettingsCustomize] + public async void PushSyncNotificationAsync_NotificationUserIdSet_SentToUser(bool withOrganizationId, + bool withDeviceIdentifier, bool deviceFound, Device device, + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, MockedHttpMessageHandler mockedHttpMessageHandler, + GlobalSettings globalSettings, Guid installationId) + { + if (!withOrganizationId) + { + notification.OrganizationId = null; + } + + ConfigureSut(sutProvider, globalSettings, installationId); + + if (withDeviceIdentifier) + { + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + if (deviceFound) + { + sutProvider.GetDependency().GetByIdentifierAsync(deviceIdentifier.ToString()) + .Returns(device); + } + } + + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + + var tokenResponse = mockedHttpMessageHandler + .When(request => + request.Method == HttpMethod.Post && request.RequestUri!.ToString().EndsWith("/connect/token")) + .RespondWith(HttpStatusCode.OK, new StringContent("{\"access_token\":\"token\"}")); + var sendResponse = mockedHttpMessageHandler + .When(request => + { + if (request.Method != HttpMethod.Post || !request.RequestUri!.ToString().EndsWith("/push/send")) + { + return false; + } + + Assert.NotNull(request.Content); + var jsonContent = Assert.IsType(request.Content); + Assert.NotNull(jsonContent.Value); + + var pushSendRequest = Assert.IsType(jsonContent.Value); + AssertRequest(pushSendRequest, + new PushSendRequestModel + { + Type = PushType.SyncNotification, + Payload = new SyncNotificationPushNotification + { + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate + }, + UserId = notification.UserId!.ToString(), + OrganizationId = null, + DeviceId = withDeviceIdentifier && deviceFound ? device.Id.ToString() : null, + Identifier = withDeviceIdentifier ? deviceIdentifier.ToString() : null, + ClientType = notification.ClientType + }, + new SyncNotificationEquals()); + return true; + }) + .RespondWith(HttpStatusCode.OK); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + Assert.Equal(1, tokenResponse.NumberOfResponses); + Assert.Equal(1, sendResponse.NumberOfResponses); + } + + [Theory] + [BitAutoData(true, true)] + [BitAutoData(true, false)] + [BitAutoData(false, true)] + [BitAutoData(false, false)] + [NotificationCustomize(false)] + [CurrentContextCustomize] + [GlobalSettingsCustomize] + public async void PushSyncNotificationAsync_NotificationOrganizationIdSet_SentToOrganization( + bool withDeviceIdentifier, bool deviceFound, Device device, + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, MockedHttpMessageHandler mockedHttpMessageHandler, + GlobalSettings globalSettings, Guid installationId) + { + notification.UserId = null; + + ConfigureSut(sutProvider, globalSettings, installationId); + + if (withDeviceIdentifier) + { + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + if (deviceFound) + { + sutProvider.GetDependency().GetByIdentifierAsync(deviceIdentifier.ToString()) + .Returns(device); + } + } + + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + + var tokenResponse = mockedHttpMessageHandler + .When(request => + request.Method == HttpMethod.Post && request.RequestUri!.ToString().EndsWith("/connect/token")) + .RespondWith(HttpStatusCode.OK, new StringContent("{\"access_token\":\"token\"}")); + var sendResponse = mockedHttpMessageHandler + .When(request => + { + if (request.Method != HttpMethod.Post || !request.RequestUri!.ToString().EndsWith("/push/send")) + { + return false; + } + + Assert.NotNull(request.Content); + var jsonContent = Assert.IsType(request.Content); + Assert.NotNull(jsonContent.Value); + + var pushSendRequest = Assert.IsType(jsonContent.Value); + AssertRequest(pushSendRequest, + new PushSendRequestModel + { + Type = PushType.SyncNotification, + Payload = new SyncNotificationPushNotification + { + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate + }, + UserId = null, + OrganizationId = notification.OrganizationId!.ToString(), + DeviceId = withDeviceIdentifier && deviceFound ? device.Id.ToString() : null, + Identifier = withDeviceIdentifier ? deviceIdentifier.ToString() : null, + ClientType = notification.ClientType + }, + new SyncNotificationEquals()); + return true; + }) + .RespondWith(HttpStatusCode.OK); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + Assert.Equal(1, tokenResponse.NumberOfResponses); + Assert.Equal(1, sendResponse.NumberOfResponses); + } + + private static void AssertRequest(PushSendRequestModel request, PushSendRequestModel expectedRequest, + IEqualityComparer expectedPayloadEquatable) + { + Assert.Equal(expectedRequest.Type, request.Type); + Assert.Equal(expectedRequest.UserId, request.UserId); + Assert.Equal(expectedRequest.OrganizationId, request.OrganizationId); + Assert.Equal(expectedRequest.DeviceId, request.DeviceId); + Assert.Equal(expectedRequest.Identifier, request.Identifier); + Assert.Equal(expectedRequest.ClientType, request.ClientType); + Assert.Equal((T)expectedRequest.Payload, (T)request.Payload, expectedPayloadEquatable); + } + + private class SyncNotificationEquals : IEqualityComparer + { + public bool Equals(SyncNotificationPushNotification? x, SyncNotificationPushNotification? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null) return false; + if (y is null) return false; + if (x.GetType() != y.GetType()) return false; + return x.Id.Equals(y.Id) && Nullable.Equals(x.UserId, y.UserId) && + Nullable.Equals(x.OrganizationId, y.OrganizationId) && x.ClientType == y.ClientType && + x.RevisionDate.Equals(y.RevisionDate); + } + + public int GetHashCode(SyncNotificationPushNotification obj) + { + return HashCode.Combine(obj.Id, obj.UserId, obj.OrganizationId, (int)obj.ClientType, obj.RevisionDate); + } + } + + private void ConfigureSut(SutProvider sutProvider, GlobalSettings globalSettings, + Guid installationId) + { + globalSettings.PushRelayBaseUri = "http://push-relay-test.localhost"; + globalSettings.Installation.IdentityUri = "http://installation-identity-test.localhost"; + globalSettings.Installation.Id = installationId; + globalSettings.Installation.Key = "installation-key"; + sutProvider.SetDependency(typeof(GlobalSettings), globalSettings) + .Create(); } }