1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-25 13:18:48 -05:00

[PM-20576] OrganizationReport - Queries and Command (#5983)

This commit is contained in:
Vijay Oommen 2025-06-24 09:13:43 -05:00 committed by GitHub
parent 494c41e3b1
commit 86a4ce5a51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 962 additions and 1 deletions

View File

@ -0,0 +1,74 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class AddOrganizationReportCommand : IAddOrganizationReportCommand
{
private readonly IOrganizationRepository _organizationRepo;
private readonly IOrganizationReportRepository _organizationReportRepo;
private ILogger<AddOrganizationReportCommand> _logger;
public AddOrganizationReportCommand(
IOrganizationRepository organizationRepository,
IOrganizationReportRepository organizationReportRepository,
ILogger<AddOrganizationReportCommand> logger)
{
_organizationRepo = organizationRepository;
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task<OrganizationReport> AddOrganizationReportAsync(AddOrganizationReportRequest request)
{
_logger.LogInformation("Adding organization report for organization {organizationId}", request.OrganizationId);
var (isValid, errorMessage) = await ValidateRequestAsync(request);
if (!isValid)
{
_logger.LogInformation("Failed to add organization {organizationId} report: {errorMessage}", request.OrganizationId, errorMessage);
throw new BadRequestException(errorMessage);
}
var organizationReport = new OrganizationReport
{
OrganizationId = request.OrganizationId,
ReportData = request.ReportData,
Date = request.Date == default ? DateTime.UtcNow : request.Date,
CreationDate = DateTime.UtcNow,
};
organizationReport.SetNewId();
var data = await _organizationReportRepo.CreateAsync(organizationReport);
_logger.LogInformation("Successfully added organization report for organization {organizationId}, {organizationReportId}",
request.OrganizationId, data.Id);
return data;
}
private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(
AddOrganizationReportRequest request)
{
// verify that the organization exists
var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);
if (organization == null)
{
return (false, "Invalid Organization");
}
// ensure that we have report data
if (string.IsNullOrWhiteSpace(request.ReportData))
{
return (false, "Report Data is required");
}
return (true, string.Empty);
}
}

View File

@ -0,0 +1,42 @@
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class DropOrganizationReportCommand : IDropOrganizationReportCommand
{
private IOrganizationReportRepository _organizationReportRepo;
private ILogger<DropOrganizationReportCommand> _logger;
public DropOrganizationReportCommand(
IOrganizationReportRepository organizationReportRepository,
ILogger<DropOrganizationReportCommand> logger)
{
_organizationReportRepo = organizationReportRepository;
_logger = logger;
}
public async Task DropOrganizationReportAsync(DropOrganizationReportRequest request)
{
_logger.LogInformation("Dropping organization report for organization {organizationId}",
request.OrganizationId);
var data = await _organizationReportRepo.GetByOrganizationIdAsync(request.OrganizationId);
if (data == null || data.Count() == 0)
{
_logger.LogInformation("No organization reports found for organization {organizationId}", request.OrganizationId);
throw new BadRequestException("No data found.");
}
data.Where(_ => request.OrganizationReportIds.Contains(_.Id)).ToList().ForEach(async _ =>
{
_logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}",
_.Id, request.OrganizationId);
await _organizationReportRepo.DeleteAsync(_);
});
}
}

View File

@ -0,0 +1,43 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class GetOrganizationReportQuery : IGetOrganizationReportQuery
{
private IOrganizationReportRepository _organizationReportRepo;
private ILogger<GetOrganizationReportQuery> _logger;
public GetOrganizationReportQuery(
IOrganizationReportRepository organizationReportRepo,
ILogger<GetOrganizationReportQuery> logger)
{
_organizationReportRepo = organizationReportRepo;
_logger = logger;
}
public async Task<IEnumerable<OrganizationReport>> GetOrganizationReportAsync(Guid organizationId)
{
if (organizationId == Guid.Empty)
{
throw new BadRequestException("OrganizationId is required.");
}
_logger.LogInformation("Fetching organization reports for organization {organizationId}", organizationId);
return await _organizationReportRepo.GetByOrganizationIdAsync(organizationId);
}
public async Task<OrganizationReport> GetLatestOrganizationReportAsync(Guid organizationId)
{
if (organizationId == Guid.Empty)
{
throw new BadRequestException("OrganizationId is required.");
}
_logger.LogInformation("Fetching latest organization report for organization {organizationId}", organizationId);
return await _organizationReportRepo.GetLatestByOrganizationIdAsync(organizationId);
}
}

View File

@ -0,0 +1,10 @@

using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IAddOrganizationReportCommand
{
Task<OrganizationReport> AddOrganizationReportAsync(AddOrganizationReportRequest request);
}

View File

@ -0,0 +1,9 @@

using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IDropOrganizationReportCommand
{
Task DropOrganizationReportAsync(DropOrganizationReportRequest request);
}

View File

@ -0,0 +1,9 @@
using Bit.Core.Dirt.Entities;
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetOrganizationReportQuery
{
Task<IEnumerable<OrganizationReport>> GetOrganizationReportAsync(Guid organizationId);
Task<OrganizationReport> GetLatestOrganizationReportAsync(Guid organizationId);
}

View File

@ -13,5 +13,8 @@ public static class ReportingServiceCollectionExtensions
services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();
services.AddScoped<IAddOrganizationReportCommand, AddOrganizationReportCommand>();
services.AddScoped<IDropOrganizationReportCommand, DropOrganizationReportCommand>();
services.AddScoped<IGetOrganizationReportQuery, GetOrganizationReportQuery>();
}
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class AddOrganizationReportRequest
{
public Guid OrganizationId { get; set; }
public string ReportData { get; set; }
public DateTime Date { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class DropOrganizationReportRequest
{
public Guid OrganizationId { get; set; }
public IEnumerable<Guid> OrganizationReportIds { get; set; }
}

View File

@ -6,5 +6,7 @@ namespace Bit.Core.Dirt.Repositories;
public interface IOrganizationReportRepository : IRepository<OrganizationReport, Guid>
{
Task<ICollection<OrganizationReport>> GetByOrganizationIdAsync(Guid organizationId);
Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId);
}

View File

@ -32,4 +32,14 @@ public class OrganizationReportRepository : Repository<OrganizationReport, Guid>
return results.ToList();
}
}
public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)
{
return await GetByOrganizationIdAsync(organizationId)
.ContinueWith(task =>
{
var reports = task.Result;
return reports.OrderByDescending(r => r.CreationDate).FirstOrDefault();
});
}
}

View File

@ -27,4 +27,22 @@ public class OrganizationReportRepository :
return Mapper.Map<ICollection<OrganizationReport>>(results);
}
}
public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.OrganizationReports
.Where(p => p.OrganizationId == organizationId)
.OrderByDescending(p => p.Date)
.Take(1)
.FirstOrDefaultAsync();
if (result == null)
return default;
return Mapper.Map<OrganizationReport>(result);
}
}
}

View File

@ -0,0 +1,133 @@

using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class AddOrganizationReportCommandTests
{
[Theory]
[BitAutoData]
public async Task AddOrganizationReportAsync_ShouldReturnOrganizationReport(
SutProvider<AddOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<AddOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(fixture.Create<Organization>());
sutProvider.GetDependency<IOrganizationReportRepository>()
.CreateAsync(Arg.Any<OrganizationReport>())
.Returns(c => c.Arg<OrganizationReport>());
// Act
var result = await sutProvider.Sut.AddOrganizationReportAsync(request);
// Assert
Assert.NotNull(result);
}
[Theory]
[BitAutoData]
public async Task AddOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowError(
SutProvider<AddOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<AddOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(null as Organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AddOrganizationReportAsync_WithInvalidUrl_ShouldThrowError(
SutProvider<AddOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<AddOrganizationReportRequest>()
.Without(_ => _.ReportData)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(fixture.Create<Organization>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));
Assert.Equal("Report Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AddOrganizationReportAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError(
SutProvider<AddOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<AddOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(null as Organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AddOrganizationReportAsync_Multiples_WithInvalidUrl_ShouldThrowError(
SutProvider<AddOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<AddOrganizationReportRequest>()
.Without(_ => _.ReportData)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(fixture.Create<Organization>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));
Assert.Equal("Report Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AddOrganizationReportAsync_WithNullOrganizationId_ShouldThrowError(
SutProvider<AddOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<AddOrganizationReportRequest>()
.With(x => x.OrganizationId, default(Guid))
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
}

View File

@ -0,0 +1,194 @@
using AutoFixture;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class DeleteOrganizationReportCommandTests
{
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withValidRequest_Success(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var OrganizationReports = fixture.CreateMany<OrganizationReport>(2).ToList();
// only take one id from the list - we only want to drop one record
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds,
OrganizationReports.Select(x => x.Id).Take(1).ToList())
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(OrganizationReports);
// Act
await sutProvider.Sut.DropOrganizationReportAsync(request);
// Assert
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.DeleteAsync(Arg.Is<OrganizationReport>(_ =>
request.OrganizationReportIds.Contains(_.Id)));
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withValidRequest_nothingToDrop(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var OrganizationReports = fixture.CreateMany<OrganizationReport>(2).ToList();
// we are passing invalid data
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds, new List<Guid> { Guid.NewGuid() })
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(OrganizationReports);
// Act
await sutProvider.Sut.DropOrganizationReportAsync(request);
// Assert
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(0)
.DeleteAsync(Arg.Any<OrganizationReport>());
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withNodata_fails(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
// we are passing invalid data
var request = fixture.Build<DropOrganizationReportRequest>()
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(null as List<OrganizationReport>);
// Act
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DropOrganizationReportAsync(request));
// Assert
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(0)
.DeleteAsync(Arg.Any<OrganizationReport>());
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withInvalidOrganizationId_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<DropOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(null as List<OrganizationReport>);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withInvalidOrganizationReportId_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<DropOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(new List<OrganizationReport>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withNullOrganizationId_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationId, default(Guid))
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withNullOrganizationReportIds_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds, default(List<Guid>))
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withEmptyOrganizationReportIds_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds, new List<Guid>())
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withEmptyRequest_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var request = new DropOrganizationReportRequest();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
}

View File

@ -0,0 +1,188 @@
using AutoFixture;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class GetOrganizationReportQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(fixture.CreateMany<OrganizationReport>(2).ToList());
// Act
var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId);
// Assert
Assert.NotNull(result);
Assert.True(result.Count() == 2);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
.Returns(new List<OrganizationReport>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetLatestByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(fixture.Create<OrganizationReport>());
// Act
var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId);
// Assert
Assert.NotNull(result);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetLatestByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
.Returns(default(OrganizationReport));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithNoReports_ShouldReturnEmptyList(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(new List<OrganizationReport>());
// Act
var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithNoReports_ShouldReturnNull(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetLatestByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(default(OrganizationReport));
// Act
var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = default(Guid);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = default(Guid);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = Guid.Empty;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = Guid.Empty;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
}

View File

@ -92,6 +92,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
cfg.AddProfile<TransactionMapperProfile>();
cfg.AddProfile<UserMapperProfile>();
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
cfg.AddProfile<OrganizationReportProfile>();
})
.CreateMapper()));

View File

@ -0,0 +1,80 @@
using AutoFixture;
using AutoFixture.Kernel;
using Bit.Core.Dirt.Entities;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture;
internal class OrganizationReportBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var type = request as Type;
if (type == null || type != typeof(OrganizationReport))
{
return new NoSpecimen();
}
var fixture = new Fixture();
var obj = fixture.WithAutoNSubstitutions().Create<OrganizationReport>();
return obj;
}
}
internal class EfOrganizationReport : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new IgnoreVirtualMembersCustomization());
fixture.Customizations.Add(new GlobalSettingsBuilder());
fixture.Customizations.Add(new OrganizationReportBuilder());
fixture.Customizations.Add(new OrganizationBuilder());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationReportRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationRepository>());
}
}
internal class EfOrganizationReportApplicableToUser : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new IgnoreVirtualMembersCustomization());
fixture.Customizations.Add(new GlobalSettingsBuilder());
fixture.Customizations.Add(new OrganizationReportBuilder());
fixture.Customizations.Add(new OrganizationBuilder());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationReportRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<UserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationUserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<ProviderRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<ProviderUserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<ProviderOrganizationRepository>());
}
}
internal class EfOrganizationReportAutoDataAttribute : CustomAutoDataAttribute
{
public EfOrganizationReportAutoDataAttribute() : base(new SutProviderCustomization(), new EfOrganizationReport()) { }
}
internal class EfOrganizationReportApplicableToUserInlineAutoDataAttribute : InlineCustomAutoDataAttribute
{
public EfOrganizationReportApplicableToUserInlineAutoDataAttribute(params object[] values)
: base(new[] { typeof(SutProviderCustomization), typeof(EfOrganizationReportApplicableToUser) }, values) { }
}
internal class InlineEfOrganizationReportAutoDataAttribute : InlineCustomAutoDataAttribute
{
public InlineEfOrganizationReportAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
typeof(EfPolicy) }, values)
{ }
}

View File

@ -0,0 +1,131 @@
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Infrastructure.Dapper.Dirt;
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
using Xunit;
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
namespace Bit.Infrastructure.EFIntegration.Test.Dirt.Repositories;
public class OrganizationReportRepositoryTests
{
[CiSkippedTheory, EfOrganizationReportAutoData]
public async Task CreateAsync_ShouldCreateReport_WhenValidDataProvided(
OrganizationReport report,
Organization organization,
List<EntityFramework.Dirt.Repositories.OrganizationReportRepository> suts,
List<EfRepo.OrganizationRepository> efOrganizationRepos,
OrganizationReportRepository sqlOrganizationReportRepo,
SqlRepo.OrganizationRepository sqlOrganizationRepo)
{
var records = new List<OrganizationReport>();
foreach (var sut in suts)
{
var i = suts.IndexOf(sut);
var efOrganization = await efOrganizationRepos[i].CreateAsync(organization);
sut.ClearChangeTracking();
report.OrganizationId = efOrganization.Id;
var postEfOrganizationReport = await sut.CreateAsync(report);
sut.ClearChangeTracking();
var savedOrganizationReport = await sut.GetByIdAsync(postEfOrganizationReport.Id);
records.Add(savedOrganizationReport);
}
var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organization);
report.OrganizationId = sqlOrganization.Id;
var sqlOrgnizationReportRecord = await sqlOrganizationReportRepo.CreateAsync(report);
var savedSqlOrganizationReport = await sqlOrganizationReportRepo.GetByIdAsync(sqlOrgnizationReportRecord.Id);
records.Add(savedSqlOrganizationReport);
Assert.True(records.Count == 4);
}
[CiSkippedTheory, EfOrganizationReportAutoData]
public async Task RetrieveByOrganisation_Works(
OrganizationReportRepository sqlPasswordHealthReportApplicationRepo,
SqlRepo.OrganizationRepository sqlOrganizationRepo)
{
var (firstOrg, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
var (secondOrg, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
var firstSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(firstOrg.Id);
var nextSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(secondOrg.Id);
Assert.True(firstSetOfRecords.Count == 1 && firstSetOfRecords.First().OrganizationId == firstOrg.Id);
Assert.True(nextSetOfRecords.Count == 1 && nextSetOfRecords.First().OrganizationId == secondOrg.Id);
}
[CiSkippedTheory, EfOrganizationReportAutoData]
public async Task Delete_Works(
List<EntityFramework.Dirt.Repositories.OrganizationReportRepository> suts,
List<EfRepo.OrganizationRepository> efOrganizationRepos,
OrganizationReportRepository sqlOrganizationReportRepo,
SqlRepo.OrganizationRepository sqlOrganizationRepo)
{
var fixture = new Fixture();
var rawOrg = fixture.Build<Organization>().Create();
var rawRecord = fixture.Build<OrganizationReport>()
.With(_ => _.OrganizationId, rawOrg.Id)
.Create();
var dbRecords = new List<OrganizationReport>();
foreach (var sut in suts)
{
var i = suts.IndexOf(sut);
// create a new organization for each repository
var organization = await efOrganizationRepos[i].CreateAsync(rawOrg);
// map the organization Id and use Upsert to save new record
rawRecord.OrganizationId = organization.Id;
rawRecord = await sut.CreateAsync(rawRecord);
sut.ClearChangeTracking();
// apply update using Upsert to make changes to db
await sut.DeleteAsync(rawRecord);
sut.ClearChangeTracking();
// retrieve the data and add to the list for assertions
var recordFromDb = await sut.GetByIdAsync(rawRecord.Id);
dbRecords.Add(recordFromDb);
sut.ClearChangeTracking();
}
// sql - create new records
var (org, organizationReport) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo);
await sqlOrganizationReportRepo.DeleteAsync(organizationReport);
var sqlDbRecord = await sqlOrganizationReportRepo.GetByIdAsync(organizationReport.Id);
dbRecords.Add(sqlDbRecord);
// assertions
// all records should be null - as they were deleted before querying
Assert.True(dbRecords.Where(_ => _ == null).Count() == 4);
}
private async Task<(Organization, OrganizationReport)> CreateOrganizationAndReportAsync(
IOrganizationRepository orgRepo,
IOrganizationReportRepository orgReportRepo)
{
var fixture = new Fixture();
var organization = fixture.Create<Organization>();
var orgReportRecord = fixture.Build<OrganizationReport>()
.With(x => x.OrganizationId, organization.Id)
.Create();
organization = await orgRepo.CreateAsync(organization);
orgReportRecord = await orgReportRepo.CreateAsync(orgReportRecord);
return (organization, orgReportRecord);
}
}

View File

@ -30,7 +30,6 @@
<ItemGroup>
<Folder Include="AutoFixture" />
<Folder Include="Tools\Repositories\" />
</ItemGroup>
</Project>