mirror of
https://github.com/bitwarden/server.git
synced 2025-03-02 04:11:04 +01:00
Merge branch 'main' into km/userkey-rotation-v2
This commit is contained in:
commit
0716858212
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -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 }}
|
||||
|
||||
|
2
.github/workflows/scan.yml
vendored
2
.github/workflows/scan.yml
vendored
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required disabled="@(canEditName ? null : "disabled")">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
|
@ -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<DisplayAttribute>()?.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<DisplayAttribute>()?.GetName();
|
||||
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||
_currentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Device> GetKnownDeviceAsync(User user, Device device)
|
||||
{
|
||||
if (user == null || device == null)
|
||||
|
@ -46,27 +46,17 @@ public class OrganizationDomainRepository : Repository<Core.Entities.Organizatio
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var domains = await dbContext.OrganizationDomains
|
||||
.Where(x => 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<List<Core.Entities.OrganizationDomain>>(results);
|
||||
return Mapper.Map<List<Core.Entities.OrganizationDomain>>(pastDomains);
|
||||
}
|
||||
|
||||
public async Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email)
|
||||
|
@ -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<IEnumerable<EventModel>>("collect",
|
||||
[
|
||||
new EventModel
|
||||
{
|
||||
Type = EventType.User_ClientExportedVault,
|
||||
Date = DateTime.UtcNow,
|
||||
},
|
||||
]);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
29
test/Events.IntegrationTest/Events.IntegrationTest.csproj
Normal file
29
test/Events.IntegrationTest/Events.IntegrationTest.csproj
Normal file
@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Events\Events.csproj" />
|
||||
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
57
test/Events.IntegrationTest/EventsApplicationFactory.cs
Normal file
57
test/Events.IntegrationTest/EventsApplicationFactory.cs
Normal file
@ -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<Startup>
|
||||
{
|
||||
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<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for registering and logging in to a new account
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
1
test/Events.IntegrationTest/GlobalUsings.cs
Normal file
1
test/Events.IntegrationTest/GlobalUsings.cs
Normal file
@ -0,0 +1 @@
|
||||
global using Xunit;
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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<T> : WebApplicationFactory<T>
|
||||
// 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<ILicensingService, NoopLicensingService>();
|
||||
Replace<ILicensingService, NoopLicensingService>(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<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
Replace<IPushRegistrationService, NoopPushRegistrationService>(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<IEventWriteService, RepositoryEventWriteService>();
|
||||
Replace<IEventWriteService, RepositoryEventWriteService>(services);
|
||||
|
||||
var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository));
|
||||
services.Remove(eventRepositoryService);
|
||||
services.AddSingleton<IEventRepository, EventRepository>();
|
||||
Replace<IEventRepository, EventRepository>(services);
|
||||
|
||||
var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService));
|
||||
services.Remove(mailDeliveryService);
|
||||
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
||||
Replace<IMailDeliveryService, NoopMailDeliveryService>(services);
|
||||
|
||||
var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService));
|
||||
services.Remove(captchaValidationService);
|
||||
services.AddSingleton<ICaptchaValidationService, NoopCaptchaValidationService>();
|
||||
Replace<ICaptchaValidationService, NoopCaptchaValidationService>(services);
|
||||
|
||||
// TODO: Install and use azurite in CI pipeline
|
||||
var installationDeviceRepository =
|
||||
services.First(sd => sd.ServiceType == typeof(IInstallationDeviceRepository));
|
||||
services.Remove(installationDeviceRepository);
|
||||
services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();
|
||||
Replace<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>(services);
|
||||
|
||||
// TODO: Install and use azurite in CI pipeline
|
||||
var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService));
|
||||
services.Remove(referenceEventService);
|
||||
services.AddSingleton<IReferenceEventService, NoopReferenceEventService>();
|
||||
Replace<IReferenceEventService, NoopReferenceEventService>(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<T> : WebApplicationFactory<T>
|
||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||
|
||||
// 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<IPaymentService>());
|
||||
Replace(services, Substitute.For<IPaymentService>());
|
||||
|
||||
var organizationBillingService =
|
||||
services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService));
|
||||
services.Remove(organizationBillingService);
|
||||
services.AddSingleton(Substitute.For<IOrganizationBillingService>());
|
||||
Replace(services, Substitute.For<IOrganizationBillingService>());
|
||||
});
|
||||
|
||||
foreach (var configureTestService in _configureTestServices)
|
||||
@ -261,6 +240,35 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
}
|
||||
}
|
||||
|
||||
private static void Replace<TService, TNewImplementation>(IServiceCollection services)
|
||||
where TService : class
|
||||
where TNewImplementation : class, TService
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.AddSingleton<TService, TNewImplementation>();
|
||||
}
|
||||
|
||||
private static void Replace<TService>(IServiceCollection services, TService implementation)
|
||||
where TService : class
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.AddSingleton<TService>(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();
|
||||
|
Loading…
Reference in New Issue
Block a user