diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Tools/Controllers/ReportsController.cs index 9f465c7b8..4c0a802da 100644 --- a/src/Api/Tools/Controllers/ReportsController.cs +++ b/src/Api/Tools/Controllers/ReportsController.cs @@ -7,7 +7,6 @@ using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Tools.ReportFeatures.Requests; -using Bit.Core.Tools.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -21,18 +20,21 @@ public class ReportsController : Controller private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; + private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; public ReportsController( ICurrentContext currentContext, IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, - IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery + IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery, + IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand ) { _currentContext = currentContext; _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; + _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; } /// @@ -161,4 +163,26 @@ public class ReportsController : Controller return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests); } + + /// + /// Drops a record from PasswordHealthReportApplication + /// + /// + /// A single instance of DropPasswordHealthReportApplicationRequest + /// { OrganizationId, array of PasswordHealthReportApplicationIds } + /// + /// + /// If user does not have access to the organization + /// If the organization does not have any records + [HttpDelete("password-health-report-application")] + public async Task DropPasswordHealthReportApplication( + [FromBody] DropPasswordHealthReportApplicationRequest request) + { + if (!await _currentContext.AccessReports(request.OrganizationId)) + { + throw new NotFoundException(); + } + + await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request); + } } diff --git a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs index c6bdb4417..b191799ba 100644 --- a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs @@ -2,8 +2,8 @@ using Bit.Core.Repositories; using Bit.Core.Tools.Entities; using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Requests; namespace Bit.Core.Tools.ReportFeatures; diff --git a/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs new file mode 100644 index 000000000..73a8f84e6 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs @@ -0,0 +1,31 @@ +using Bit.Core.Exceptions; +using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; +using Bit.Core.Tools.Repositories; + +namespace Bit.Core.Tools.ReportFeatures; + +public class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand +{ + private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo; + + public DropPasswordHealthReportApplicationCommand( + IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepository) + { + _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepository; + } + + public async Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request) + { + var data = await _passwordHealthReportApplicationRepo.GetByOrganizationIdAsync(request.OrganizationId); + if (data == null) + { + throw new BadRequestException("Organization does not have any records."); + } + + data.Where(_ => request.PasswordHealthReportApplicationIds.Contains(_.Id)).ToList().ForEach(async _ => + { + await _passwordHealthReportApplicationRepo.DeleteAsync(_); + }); + } +} diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs index 86e7d44c7..9d145a79b 100644 --- a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Requests; +using Bit.Core.Tools.ReportFeatures.Requests; namespace Bit.Core.Tools.ReportFeatures.Interfaces; diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs new file mode 100644 index 000000000..0adf09cab --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Tools.ReportFeatures.Requests; + +namespace Bit.Core.Tools.ReportFeatures.Interfaces; + +public interface IDropPasswordHealthReportApplicationCommand +{ + Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request); +} + diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs index 85e5388a0..4970f0515 100644 --- a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -11,5 +11,6 @@ public static class ReportingServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs b/src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs similarity index 72% rename from src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs rename to src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs index f5a1d35ea..dfc544b1c 100644 --- a/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs +++ b/src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Tools.Requests; +namespace Bit.Core.Tools.ReportFeatures.Requests; public class AddPasswordHealthReportApplicationRequest { diff --git a/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs b/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs new file mode 100644 index 000000000..1464e68f0 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Tools.ReportFeatures.Requests; + +public class DropPasswordHealthReportApplicationRequest +{ + public Guid OrganizationId { get; set; } + public IEnumerable PasswordHealthReportApplicationIds { get; set; } +} diff --git a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs b/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs index 07d37f672..3057e1064 100644 --- a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs @@ -1,7 +1,9 @@ -using Bit.Api.Tools.Controllers; +using AutoFixture; +using Bit.Api.Tools.Controllers; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -45,5 +47,98 @@ public class ReportsControllerTests .Received(0); } + [Theory, BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_withAccess_success(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + // Act + var request = new Api.Tools.Models.PasswordHealthReportApplicationModel + { + OrganizationId = Guid.NewGuid(), + Url = "https://example.com", + }; + await sutProvider.Sut.AddPasswordHealthReportApplication(request); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .AddPasswordHealthReportApplicationAsync(Arg.Is(_ => + _.OrganizationId == request.OrganizationId && _.Url == request.Url)); + } + + [Theory, BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_multiple_withAccess_success( + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + + // Act + var fixture = new Fixture(); + var request = fixture.CreateMany(2); + await sutProvider.Sut.AddPasswordHealthReportApplications(request); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .AddPasswordHealthReportApplicationAsync(Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_withoutAccess(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); + + // Act + var request = new Api.Tools.Models.PasswordHealthReportApplicationModel + { + OrganizationId = Guid.NewGuid(), + Url = "https://example.com", + }; + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.AddPasswordHealthReportApplication(request)); + + // Assert + _ = sutProvider.GetDependency() + .Received(0); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withoutAccess(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); + + // Act + var fixture = new Fixture(); + var request = fixture.Create(); + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.AddPasswordHealthReportApplication(request)); + + // Assert + _ = sutProvider.GetDependency() + .Received(0); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withAccess_success(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + + // Act + var fixture = new Fixture(); + var request = fixture.Create(); + await sutProvider.Sut.DropPasswordHealthReportApplication(request); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .DropPasswordHealthReportApplicationAsync(Arg.Is(_ => + _.OrganizationId == request.OrganizationId && + _.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds)); + } } diff --git a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs index 8c3a68fdc..5018123e2 100644 --- a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs +++ b/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs @@ -4,8 +4,8 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Tools.Entities; using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Requests; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs new file mode 100644 index 000000000..c459d0e81 --- /dev/null +++ b/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs @@ -0,0 +1,104 @@ +using AutoFixture; +using Bit.Core.Exceptions; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.ReportFeatures.Requests; +using Bit.Core.Tools.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.ReportFeatures; + +[SutProviderCustomize] +public class DeletePasswordHealthReportApplicationCommandTests +{ + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_Success( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var passwordHealthReportApplications = 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.PasswordHealthReportApplicationIds, + passwordHealthReportApplications.Select(x => x.Id).Take(1).ToList()) + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(passwordHealthReportApplications); + + // Act + await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Is(_ => + request.PasswordHealthReportApplicationIds.Contains(_.Id))); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_nothingToDrop( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var passwordHealthReportApplications = fixture.CreateMany(2).ToList(); + // we are passing invalid data + var request = fixture.Build() + .With(x => x.PasswordHealthReportApplicationIds, new List { Guid.NewGuid() }) + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(passwordHealthReportApplications); + + // Act + await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(0) + .DeleteAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_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.DropPasswordHealthReportApplicationAsync(request)); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(0) + .DeleteAsync(Arg.Any()); + } +}