From 86a4ce5a51401705cb8a9fde7c74a5e19c7a3b8d Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 24 Jun 2025 09:13:43 -0500 Subject: [PATCH] [PM-20576] OrganizationReport - Queries and Command (#5983) --- .../AddOrganizationReportCommand.cs | 74 +++++++ .../DropOrganizationReportCommand.cs | 42 ++++ .../GetOrganizationReportQuery.cs | 43 ++++ .../IAddOrganizationReportCommand.cs | 10 + .../IDropOrganizationReportCommand.cs | 9 + .../Interfaces/IGetOrganizationReportQuery.cs | 9 + .../ReportingServiceCollectionExtensions.cs | 3 + .../Requests/AddOrganizationReportRequest.cs | 8 + .../Requests/DropOrganizationReportRequest.cs | 7 + .../IOrganizationReportRepository.cs | 2 + .../Dirt/OrganizationReportRepository.cs | 10 + .../OrganizationReportRepository.cs | 18 ++ .../AddOrganizationReportCommandTests.cs | 133 ++++++++++++ .../DeleteOrganizationReportCommandTests.cs | 194 ++++++++++++++++++ .../GetOrganizationReportQueryTests.cs | 188 +++++++++++++++++ .../EntityFrameworkRepositoryFixtures.cs | 1 + .../AutoFixture/OrganizationReportFixture.cs | 80 ++++++++ .../OrganizationReportRepositoryTests.cs | 131 ++++++++++++ .../Infrastructure.EFIntegration.Test.csproj | 1 - 19 files changed, 962 insertions(+), 1 deletion(-) create mode 100644 src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs create mode 100644 test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs create mode 100644 test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs diff --git a/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs new file mode 100644 index 0000000000..66d25cdf56 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs @@ -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 _logger; + + public AddOrganizationReportCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository, + ILogger logger) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + _logger = logger; + } + + public async Task 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); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs new file mode 100644 index 0000000000..e382788273 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs @@ -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 _logger; + + public DropOrganizationReportCommand( + IOrganizationReportRepository organizationReportRepository, + ILogger 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(_); + }); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs new file mode 100644 index 0000000000..e536fdfddc --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs @@ -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 _logger; + + public GetOrganizationReportQuery( + IOrganizationReportRepository organizationReportRepo, + ILogger logger) + { + _organizationReportRepo = organizationReportRepo; + _logger = logger; + } + + public async Task> 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 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); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs new file mode 100644 index 0000000000..3677b9794b --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs @@ -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 AddOrganizationReportAsync(AddOrganizationReportRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs new file mode 100644 index 0000000000..1ed9059f56 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs @@ -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); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs new file mode 100644 index 0000000000..f596e8f517 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IGetOrganizationReportQuery +{ + Task> GetOrganizationReportAsync(Guid organizationId); + Task GetLatestOrganizationReportAsync(Guid organizationId); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs index 4339d0f2f4..a20c7a3e8f 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -13,5 +13,8 @@ public static class ReportingServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs new file mode 100644 index 0000000000..ca892cddde --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs @@ -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; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs new file mode 100644 index 0000000000..cc889fe351 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class DropOrganizationReportRequest +{ + public Guid OrganizationId { get; set; } + public IEnumerable OrganizationReportIds { get; set; } +} diff --git a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs index 6efb6cf23a..e7979ca4b7 100644 --- a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs @@ -6,5 +6,7 @@ namespace Bit.Core.Dirt.Repositories; public interface IOrganizationReportRepository : IRepository { Task> GetByOrganizationIdAsync(Guid organizationId); + + Task GetLatestByOrganizationIdAsync(Guid organizationId); } diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs index 88478c31ae..7a5fe1c8c2 100644 --- a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs @@ -32,4 +32,14 @@ public class OrganizationReportRepository : Repository return results.ToList(); } } + + public async Task GetLatestByOrganizationIdAsync(Guid organizationId) + { + return await GetByOrganizationIdAsync(organizationId) + .ContinueWith(task => + { + var reports = task.Result; + return reports.OrderByDescending(r => r.CreationDate).FirstOrDefault(); + }); + } } diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs index 7c7bd56dae..416fd91933 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs @@ -27,4 +27,22 @@ public class OrganizationReportRepository : return Mapper.Map>(results); } } + + public async Task 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(result); + } + } } diff --git a/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..618558142b --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs @@ -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 sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(c => c.Arg()); + + // Act + var result = await sutProvider.Sut.AddOrganizationReportAsync(request); + + // Assert + Assert.NotNull(result); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(null as Organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_WithInvalidUrl_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .Without(_ => _.ReportData) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(null as Organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_Multiples_WithInvalidUrl_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .Without(_ => _.ReportData) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_WithNullOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationId, default(Guid)) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..f6a5c13be9 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs @@ -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 sutProvider) + { + // Arrange + var fixture = new Fixture(); + var OrganizationReports = fixture.CreateMany(2).ToList(); + // only take one id from the list - we only want to drop one record + var request = fixture.Build() + .With(x => x.OrganizationReportIds, + OrganizationReports.Select(x => x.Id).Take(1).ToList()) + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(OrganizationReports); + + // Act + await sutProvider.Sut.DropOrganizationReportAsync(request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Is(_ => + request.OrganizationReportIds.Contains(_.Id))); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withValidRequest_nothingToDrop( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var OrganizationReports = fixture.CreateMany(2).ToList(); + // we are passing invalid data + var request = fixture.Build() + .With(x => x.OrganizationReportIds, new List { Guid.NewGuid() }) + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(OrganizationReports); + + // Act + await sutProvider.Sut.DropOrganizationReportAsync(request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(0) + .DeleteAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withNodata_fails( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + // we are passing invalid data + var request = fixture.Build() + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(null as List); + + // Act + await Assert.ThrowsAsync(() => + sutProvider.Sut.DropOrganizationReportAsync(request)); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(0) + .DeleteAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(null as List); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); + Assert.Equal("No data found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withInvalidOrganizationReportId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); + Assert.Equal("No data found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withNullOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationId, default(Guid)) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); + Assert.Equal("No data found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withNullOrganizationReportIds_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationReportIds, default(List)) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); + Assert.Equal("No data found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withEmptyOrganizationReportIds_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationReportIds, new List()) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); + Assert.Equal("No data found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DropOrganizationReportAsync_withEmptyRequest_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var request = new DropOrganizationReportRequest(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); + Assert.Equal("No data found.", exception.Message); + } + +} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs new file mode 100644 index 0000000000..19d020be12 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs @@ -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 sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(fixture.CreateMany(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 sutProvider) + { + // Arrange + var fixture = new Fixture(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Is(x => x == Guid.Empty)) + .Returns(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + sutProvider.GetDependency() + .GetLatestByOrganizationIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act + var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId); + + // Assert + Assert.NotNull(result); + } + + [Theory] + [BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + sutProvider.GetDependency() + .GetLatestByOrganizationIdAsync(Arg.Is(x => x == Guid.Empty)) + .Returns(default(OrganizationReport)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportAsync_WithNoReports_ShouldReturnEmptyList( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(new List()); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + [Theory] + [BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithNoReports_ShouldReturnNull( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + sutProvider.GetDependency() + .GetLatestByOrganizationIdAsync(Arg.Any()) + .Returns(default(OrganizationReport)); + + // Act + var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId); + + // Assert + Assert.Null(result); + } + [Theory] + [BitAutoData] + public async Task GetOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = default(Guid); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } + [Theory] + [BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = default(Guid); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } + [Theory] + [BitAutoData] + public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = Guid.Empty; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } + [Theory] + [BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = Guid.Empty; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } +} diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index 4a56d2cb22..5c7b3ed99d 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -92,6 +92,7 @@ public class EfRepositoryListBuilder : ISpecimenBuilder where T : BaseEntityF cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); + cfg.AddProfile(); }) .CreateMapper())); diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs new file mode 100644 index 0000000000..b5b2626d7a --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs @@ -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(); + 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()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } +} + +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()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } +} + +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) + { } +} diff --git a/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs new file mode 100644 index 0000000000..dd2adc0970 --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs @@ -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 suts, + List efOrganizationRepos, + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var records = new List(); + 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 suts, + List efOrganizationRepos, + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var fixture = new Fixture(); + var rawOrg = fixture.Build().Create(); + var rawRecord = fixture.Build() + .With(_ => _.OrganizationId, rawOrg.Id) + .Create(); + var dbRecords = new List(); + + 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(); + + var orgReportRecord = fixture.Build() + .With(x => x.OrganizationId, organization.Id) + .Create(); + + organization = await orgRepo.CreateAsync(organization); + orgReportRecord = await orgReportRepo.CreateAsync(orgReportRecord); + + return (organization, orgReportRecord); + } +} diff --git a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj index 8c2eb50c5b..e63d3d7419 100644 --- a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj +++ b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj @@ -30,7 +30,6 @@ -