diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d64612aba..3b96eeb468 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -314,7 +314,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 156ebee165..ec2eb7789a 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: cx_result.sarif diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 75e7d7fade..e9aff53f8e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -313,6 +315,10 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -363,6 +369,7 @@ Global {81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 3fdef169b4..60a5a39612 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -421,6 +421,11 @@ public class OrganizationsController : Controller private void UpdateOrganization(Organization organization, OrganizationEditModel model) { + if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit)) + { + organization.Name = WebUtility.HtmlEncode(model.Name); + } + if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox)) { organization.Enabled = model.Enabled; diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index cdc7608675..aeff65c900 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -12,6 +12,7 @@ var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View); var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View); var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View); + var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit); var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox); var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit); var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit); @@ -28,7 +29,7 @@
- +
diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 20c500c061..4edcd742b4 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -22,6 +22,7 @@ public enum Permission Org_List_View, Org_OrgInformation_View, Org_GeneralDetails_View, + Org_Name_Edit, Org_CheckEnabledBox, Org_BusinessInformation_View, Org_InitiateTrial, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 4b5a4e3802..3b510781be 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -24,6 +24,7 @@ public static class RolePermissionMapping Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -71,6 +72,7 @@ public static class RolePermissionMapping Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -116,6 +118,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -148,6 +151,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -185,6 +189,7 @@ public static class RolePermissionMapping Permission.User_Premium_View, Permission.User_Licensing_View, Permission.User_Licensing_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 71330c6455..a510a3b4bd 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -107,6 +107,7 @@ public static class FeatureFlagKeys public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; + public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 1b148c5974..fee10e10ff 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -85,28 +85,17 @@ public class DeviceValidator( } } - // At this point we have established either new device verification is not required or the NewDeviceOtp is valid + // At this point we have established either new device verification is not required or the NewDeviceOtp is valid, + // so we save the device to the database and proceed with authentication requestDevice.UserId = context.User.Id; await _deviceService.SaveAsync(requestDevice); context.Device = requestDevice; - // backwards compatibility -- If NewDeviceVerification not enabled send the new login emails - // PM-13340: removal Task; remove entire if block emails should no longer be sent - if (!_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)) + if (!_globalSettings.DisableEmailNewDevice) { - // This ensures the user doesn't receive a "new device" email on the first login - var now = DateTime.UtcNow; - if (now - context.User.CreationDate > TimeSpan.FromMinutes(10)) - { - var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName(); - if (!_globalSettings.DisableEmailNewDevice) - { - await _mailService.SendNewDeviceLoggedInEmail(context.User.Email, deviceType, now, - _currentContext.IpAddress); - } - } + await SendNewDeviceLoginEmail(context.User, requestDevice); } + return true; } @@ -174,6 +163,19 @@ public class DeviceValidator( return DeviceValidationResultType.NewDeviceVerificationRequired; } + private async Task SendNewDeviceLoginEmail(User user, Device requestDevice) + { + // Ensure that the user doesn't receive a "new device" email on the first login + var now = DateTime.UtcNow; + if (now - user.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, + _currentContext.IpAddress); + } + } + public async Task GetKnownDeviceAsync(User user, Device device) { if (user == null || device == null) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index e339c13351..50d791b81b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -46,27 +46,17 @@ public class OrganizationDomainRepository : Repository x.VerifiedDate == null - && x.JobRunCount != 3 - && x.NextRunDate.Year == date.Year - && x.NextRunDate.Month == date.Month - && x.NextRunDate.Day == date.Day - && x.NextRunDate.Hour == date.Hour) - .AsNoTracking() + var start36HoursWindow = date.AddHours(-36); + var end36HoursWindow = date; + + var pastDomains = await dbContext.OrganizationDomains + .Where(x => x.NextRunDate >= start36HoursWindow + && x.NextRunDate <= end36HoursWindow + && x.VerifiedDate == null + && x.JobRunCount != 3) .ToListAsync(); - //Get records that have ignored/failed by the background service - var pastDomains = dbContext.OrganizationDomains - .AsEnumerable() - .Where(x => (date - x.NextRunDate).TotalHours > 36 - && x.VerifiedDate == null - && x.JobRunCount != 3) - .ToList(); - - var results = domains.Union(pastDomains); - - return Mapper.Map>(results); + return Mapper.Map>(pastDomains); } public async Task GetOrganizationDomainSsoDetailsAsync(string email) diff --git a/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs new file mode 100644 index 0000000000..7f86758144 --- /dev/null +++ b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs @@ -0,0 +1,29 @@ +using System.Net.Http.Json; +using Bit.Core.Enums; +using Bit.Events.Models; + +namespace Bit.Events.IntegrationTest.Controllers; + +public class CollectControllerTests +{ + // This is a very simple test, and should be updated to assert more things, but for now + // it ensures that the events startup doesn't throw any errors with fairly basic configuration. + [Fact] + public async Task Post_Works() + { + var eventsApplicationFactory = new EventsApplicationFactory(); + var (accessToken, _) = await eventsApplicationFactory.LoginWithNewAccount(); + var client = eventsApplicationFactory.CreateAuthedClient(accessToken); + + var response = await client.PostAsJsonAsync>("collect", + [ + new EventModel + { + Type = EventType.User_ClientExportedVault, + Date = DateTime.UtcNow, + }, + ]); + + response.EnsureSuccessStatusCode(); + } +} diff --git a/test/Events.IntegrationTest/Events.IntegrationTest.csproj b/test/Events.IntegrationTest/Events.IntegrationTest.csproj new file mode 100644 index 0000000000..0b51185298 --- /dev/null +++ b/test/Events.IntegrationTest/Events.IntegrationTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs new file mode 100644 index 0000000000..3faf5e81bf --- /dev/null +++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs @@ -0,0 +1,57 @@ +using Bit.Identity.Models.Request.Accounts; +using Bit.IntegrationTestCommon.Factories; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Events.IntegrationTest; + +public class EventsApplicationFactory : WebApplicationFactoryBase +{ + private readonly IdentityApplicationFactory _identityApplicationFactory; + private const string _connectionString = "DataSource=:memory:"; + + public EventsApplicationFactory() + { + SqliteConnection = new SqliteConnection(_connectionString); + SqliteConnection.Open(); + + _identityApplicationFactory = new IdentityApplicationFactory(); + _identityApplicationFactory.SqliteConnection = SqliteConnection; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + + builder.ConfigureTestServices(services => + { + services.Configure(JwtBearerDefaults.AuthenticationScheme, options => + { + options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler(); + }); + }); + } + + /// + /// Helper for registering and logging in to a new account + /// + public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") + { + await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + }); + + return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + SqliteConnection!.Dispose(); + } +} diff --git a/test/Events.IntegrationTest/GlobalUsings.cs b/test/Events.IntegrationTest/GlobalUsings.cs new file mode 100644 index 0000000000..9df1d42179 --- /dev/null +++ b/test/Events.IntegrationTest/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index fa3a117c55..6e6406f16b 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -227,7 +227,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsync_ExistingUserNewDeviceLogin_SendNewDeviceLoginEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -237,8 +237,6 @@ public class DeviceValidatorTests _globalSettings.DisableEmailNewDevice = false; _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(false); // set user creation to more than 10 minutes ago context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); @@ -253,7 +251,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsync_NewUserNewDeviceLogin_DoesNotSendNewDeviceLoginEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -263,8 +261,6 @@ public class DeviceValidatorTests _globalSettings.DisableEmailNewDevice = false; _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(false); // set user creation to less than 10 minutes ago context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9); @@ -279,7 +275,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsynce_DisableNewDeviceLoginEmailTrue_DoesNotSendNewDeviceEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -289,8 +285,6 @@ public class DeviceValidatorTests _globalSettings.DisableEmailNewDevice = true; _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(false); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index 8e0b502a47..ad92f40efc 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -188,4 +188,122 @@ public class OrganizationDomainRepositoryTests var expectedDomain2 = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain2.DomainName); Assert.Null(expectedDomain2); } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var within36HoursWindow = 1; + organizationDomain.SetNextRunDate(within36HoursWindow); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = organizationDomain.NextRunDate; + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.NotNull(expectedDomain); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var outside36HoursWindow = 20; + organizationDomain.SetNextRunDate(outside36HoursWindow); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = DateTimeOffset.UtcNow.Date.AddDays(1); + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.Null(expectedDomain); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var within36HoursWindow = 1; + organizationDomain.SetNextRunDate(within36HoursWindow); + organizationDomain.SetVerifiedDate(); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = DateTimeOffset.UtcNow.Date.AddDays(1); + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.Null(expectedDomain); + } } diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index d01e92ad4c..7c7f790cdc 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -14,6 +14,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -188,44 +189,27 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // QUESTION: The normal licensing service should run fine on developer machines but not in CI // should we have a fork here to leave the normal service for developers? // TODO: Eventually add the license file to CI - var licensingService = services.First(sd => sd.ServiceType == typeof(ILicensingService)); - services.Remove(licensingService); - services.AddSingleton(); + Replace(services); // FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only - var pushRegistrationService = services.First(sd => sd.ServiceType == typeof(IPushRegistrationService)); - services.Remove(pushRegistrationService); - services.AddSingleton(); + Replace(services); // Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service // instead of using Noop for this service // TODO: Install and use azurite in CI pipeline - var eventWriteService = services.First(sd => sd.ServiceType == typeof(IEventWriteService)); - services.Remove(eventWriteService); - services.AddSingleton(); + Replace(services); - var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository)); - services.Remove(eventRepositoryService); - services.AddSingleton(); + Replace(services); - var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService)); - services.Remove(mailDeliveryService); - services.AddSingleton(); + Replace(services); - var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService)); - services.Remove(captchaValidationService); - services.AddSingleton(); + Replace(services); // TODO: Install and use azurite in CI pipeline - var installationDeviceRepository = - services.First(sd => sd.ServiceType == typeof(IInstallationDeviceRepository)); - services.Remove(installationDeviceRepository); - services.AddSingleton(); + Replace(services); // TODO: Install and use azurite in CI pipeline - var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService)); - services.Remove(referenceEventService); - services.AddSingleton(); + Replace(services); // Our Rate limiter works so well that it begins to fail tests unless we carve out // one whitelisted ip. We should still test the rate limiter though and they should change the Ip @@ -245,14 +229,9 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory services.AddSingleton(); // Noop StripePaymentService - this could be changed to integrate with our Stripe test account - var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService)); - services.Remove(stripePaymentService); - services.AddSingleton(Substitute.For()); + Replace(services, Substitute.For()); - var organizationBillingService = - services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService)); - services.Remove(organizationBillingService); - services.AddSingleton(Substitute.For()); + Replace(services, Substitute.For()); }); foreach (var configureTestService in _configureTestServices) @@ -261,6 +240,35 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory } } + private static void Replace(IServiceCollection services) + where TService : class + where TNewImplementation : class, TService + { + services.RemoveAll(); + services.AddSingleton(); + } + + private static void Replace(IServiceCollection services, TService implementation) + where TService : class + { + services.RemoveAll(); + services.AddSingleton(implementation); + } + + public HttpClient CreateAuthedClient(string accessToken) + { + var handler = Server.CreateHandler((context) => + { + context.Request.Headers.Authorization = $"Bearer {accessToken}"; + }); + + return new HttpClient(handler) + { + BaseAddress = Server.BaseAddress, + Timeout = TimeSpan.FromSeconds(200), + }; + } + public DatabaseContext GetDatabaseContext() { var scope = Services.CreateScope();