From 4412e6ce2f2b100232273805a431cbc09923fc70 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Wed, 18 Jun 2025 14:14:32 -0500 Subject: [PATCH] PM-20576 Commands and Query for OrganizationReport --- .../AddOrganizationReportCommand.cs | 64 +++++++++++ .../DropOrganizationReportCommand.cs | 31 ++++++ .../GetOrganizationReportQuery.cs | 37 +++++++ .../IAddOrganizationReportCommand.cs | 10 ++ .../IDropOrganizationReportCommand.cs | 9 ++ .../Interfaces/IGetOrganizationReportQuery.cs | 9 ++ .../ReportingServiceCollectionExtensions.cs | 2 + .../Requests/AddOrganizationReportRequest.cs | 8 ++ .../Requests/DropOrganizationReportRequest.cs | 7 ++ .../AddOrganizationReportCommandTests.cs | 80 ++++++++++++++ .../DeleteOrganizationReportCommandTests.cs | 104 ++++++++++++++++++ .../GetOrganizationReportQueryTests.cs | 90 +++++++++++++++ 12 files changed, 451 insertions(+) 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 diff --git a/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs new file mode 100644 index 0000000000..00a56b6bc5 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs @@ -0,0 +1,64 @@ +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; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class AddOrganizationReportCommand : IAddOrganizationReportCommand +{ + private readonly IOrganizationRepository _organizationRepo; + private readonly IOrganizationReportRepository _organizationReportRepo; + + public AddOrganizationReportCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + } + + public async Task AddOrganizationReportAsync(AddOrganizationReportRequest request) + { + var (req, IsValid, errorMessage) = await ValidateRequestAsync(request); + if (!IsValid) + { + 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, + RevisionDate = DateTime.UtcNow + }; + + organizationReport.SetNewId(); + + var data = await _organizationReportRepo.CreateAsync(organizationReport); + return data; + } + + private async Task> ValidateRequestAsync( + AddOrganizationReportRequest request) + { + // verify that the organization exists + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return new Tuple(request, false, "Invalid Organization"); + } + + // ensure that we have a URL + if (string.IsNullOrWhiteSpace(request.ReportData)) + { + return new Tuple(request, false, "Report Data is required"); + } + + return new Tuple(request, 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..0a3810af8c --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs @@ -0,0 +1,31 @@ +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class DropOrganizationReportCommand : IDropOrganizationReportCommand +{ + private IOrganizationReportRepository _OrganizationReportRepo; + + public DropOrganizationReportCommand( + IOrganizationReportRepository OrganizationReportRepository) + { + _OrganizationReportRepo = OrganizationReportRepository; + } + + public async Task DropOrganizationReportAsync(DropOrganizationReportRequest request) + { + var data = await _OrganizationReportRepo.GetByOrganizationIdAsync(request.OrganizationId); + if (data == null) + { + throw new BadRequestException("Organization does not have any records."); + } + + data.Where(_ => request.OrganizationReportIds.Contains(_.Id)).ToList().ForEach(async _ => + { + 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..cd5393f46b --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs @@ -0,0 +1,37 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class GetOrganizationReportQuery : IGetOrganizationReportQuery +{ + private IOrganizationReportRepository _organizationReportRepo; + + public GetOrganizationReportQuery( + IOrganizationReportRepository organizationReportRepo) + { + _organizationReportRepo = organizationReportRepo; + } + + public async Task> GetOrganizationReportAsync(Guid organizationId) + { + if (organizationId == Guid.Empty) + { + throw new BadRequestException("OrganizationId is required."); + } + + return await _organizationReportRepo.GetByOrganizationIdAsync(organizationId); + } + + public async Task GetLatestOrganizationReportAsync(Guid organizationId) + { + if (organizationId == Guid.Empty) + { + throw new BadRequestException("OrganizationId is required."); + } + + 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..a73a02fa59 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -13,5 +13,7 @@ public static class ReportingServiceCollectionExtensions 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/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..a1850a88dd --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs @@ -0,0 +1,80 @@ + +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); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..a24d4268e7 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs @@ -0,0 +1,104 @@ +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()); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs new file mode 100644 index 0000000000..df8c7fee02 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs @@ -0,0 +1,90 @@ +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); + } +}