using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Repositories.Noop; using Bit.Core.Settings; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; namespace Bit.Core.Test.Platform.Push; public class PushServiceCollectionExtensionsTests { [Fact] public void AddPush_SelfHosted_NoConfig_NoEngines() { var services = Build(new Dictionary { { "GlobalSettings:SelfHosted", "true" }, { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, }); _ = services.GetRequiredService(); var engines = services.GetServices(); Assert.Empty(engines); } [Fact] public void AddPush_SelfHosted_ConfiguredForRelay_RelayEngineAdded() { var services = Build(new Dictionary { { "GlobalSettings:SelfHosted", "true" }, { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, { "GlobalSettings:Installation:Key", "some_key"}, { "GlobalSettings:PushRelayBaseUri", "https://example.com" }, }); _ = services.GetRequiredService(); var engines = services.GetServices(); var engine = Assert.Single(engines); Assert.IsType(engine); } [Fact] public void AddPush_SelfHosted_ConfiguredForApi_ApiEngineAdded() { var services = Build(new Dictionary { { "GlobalSettings:SelfHosted", "true" }, { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, { "GlobalSettings:InternalIdentityKey", "some_key"}, { "GlobalSettings:BaseServiceUri", "https://example.com" }, }); _ = services.GetRequiredService(); var engines = services.GetServices(); var engine = Assert.Single(engines); Assert.IsType(engine); } [Fact] public void AddPush_SelfHosted_ConfiguredForRelayAndApi_TwoEnginesAdded() { var services = Build(new Dictionary { { "GlobalSettings:SelfHosted", "true" }, { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, { "GlobalSettings:Installation:Key", "some_key"}, { "GlobalSettings:PushRelayBaseUri", "https://example.com" }, { "GlobalSettings:InternalIdentityKey", "some_key"}, { "GlobalSettings:BaseServiceUri", "https://example.com" }, }); _ = services.GetRequiredService(); var engines = services.GetServices(); Assert.Collection( engines, e => Assert.IsType(e), e => Assert.IsType(e) ); } [Fact] public void AddPush_Cloud_NoConfig_AddsNotificationHub() { var services = Build(new Dictionary { { "GlobalSettings:SelfHosted", "false" }, }); _ = services.GetRequiredService(); var engines = services.GetServices(); var engine = Assert.Single(engines); Assert.IsType(engine); } [Fact] public void AddPush_Cloud_HasNotificationConnectionString_TwoEngines() { var services = Build(new Dictionary { { "GlobalSettings:SelfHosted", "false" }, { "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" }, }); _ = services.GetRequiredService(); var engines = services.GetServices(); Assert.Collection( engines, e => Assert.IsType(e), e => Assert.IsType(e) ); } [Fact] public void AddPush_Cloud_CalledTwice_DoesNotAddServicesTwice() { var services = new ServiceCollection(); var config = new Dictionary { { "GlobalSettings:SelfHosted", "false" }, { "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" }, }; AddServices(services, config); var initialCount = services.Count; // Add services again AddServices(services, config); Assert.Equal(initialCount, services.Count); } private static ServiceProvider Build(Dictionary initialData) { var services = new ServiceCollection(); AddServices(services, initialData); return services.BuildServiceProvider(); } private static void AddServices(IServiceCollection services, Dictionary initialData) { // A minimal service collection is always expected to have logging, config, and global settings // pre-registered. services.AddLogging(); var config = new ConfigurationBuilder() .AddInMemoryCollection(initialData) .Build(); services.TryAddSingleton(config); var globalSettings = new GlobalSettings(); config.GetSection("GlobalSettings").Bind(globalSettings); services.TryAddSingleton(globalSettings); services.TryAddSingleton(globalSettings); // Temporary until AddPush can add it themselves directly. services.TryAddSingleton(); // Temporary until AddPush can add it themselves directly. services.TryAddSingleton(); services.AddPush(globalSettings); } private class StubDeviceRepository : IDeviceRepository { public Task ClearPushTokenAsync(Guid id) => throw new NotImplementedException(); public Task CreateAsync(Device obj) => throw new NotImplementedException(); public Task DeleteAsync(Device obj) => throw new NotImplementedException(); public Task GetByIdAsync(Guid id, Guid userId) => throw new NotImplementedException(); public Task GetByIdAsync(Guid id) => throw new NotImplementedException(); public Task GetByIdentifierAsync(string identifier) => throw new NotImplementedException(); public Task GetByIdentifierAsync(string identifier, Guid userId) => throw new NotImplementedException(); public Task> GetManyByUserIdAsync(Guid userId) => throw new NotImplementedException(); public Task> GetManyByUserIdWithDeviceAuth(Guid userId) => throw new NotImplementedException(); public Task ReplaceAsync(Device obj) => throw new NotImplementedException(); public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) => throw new NotImplementedException(); public Task UpsertAsync(Device obj) => throw new NotImplementedException(); } }