1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-30 13:33:24 +01:00

PM-15066 added drop feature and unit tests. (#5053)

This commit is contained in:
Vijay Oommen 2024-11-20 14:18:05 -06:00 committed by GitHub
parent 052235bed6
commit 92b94fd4ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 278 additions and 7 deletions

View File

@ -7,7 +7,6 @@ using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Tools.Requests;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -21,18 +20,21 @@ public class ReportsController : Controller
private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery;
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
public ReportsController( public ReportsController(
ICurrentContext currentContext, ICurrentContext currentContext,
IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery, IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery,
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
) )
{ {
_currentContext = currentContext; _currentContext = currentContext;
_memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery;
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
} }
/// <summary> /// <summary>
@ -161,4 +163,26 @@ public class ReportsController : Controller
return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests); return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests);
} }
/// <summary>
/// Drops a record from PasswordHealthReportApplication
/// </summary>
/// <param name="request">
/// A single instance of DropPasswordHealthReportApplicationRequest
/// { OrganizationId, array of PasswordHealthReportApplicationIds }
/// </param>
/// <returns></returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization does not have any records</exception>
[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);
}
} }

View File

@ -2,8 +2,8 @@
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Requests;
namespace Bit.Core.Tools.ReportFeatures; namespace Bit.Core.Tools.ReportFeatures;

View File

@ -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(_);
});
}
}

View File

@ -1,5 +1,5 @@
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Requests; using Bit.Core.Tools.ReportFeatures.Requests;
namespace Bit.Core.Tools.ReportFeatures.Interfaces; namespace Bit.Core.Tools.ReportFeatures.Interfaces;

View File

@ -0,0 +1,9 @@
using Bit.Core.Tools.ReportFeatures.Requests;
namespace Bit.Core.Tools.ReportFeatures.Interfaces;
public interface IDropPasswordHealthReportApplicationCommand
{
Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request);
}

View File

@ -11,5 +11,6 @@ public static class ReportingServiceCollectionExtensions
services.AddScoped<IMemberAccessCipherDetailsQuery, MemberAccessCipherDetailsQuery>(); services.AddScoped<IMemberAccessCipherDetailsQuery, MemberAccessCipherDetailsQuery>();
services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>(); services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>(); services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();
} }
} }

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Tools.Requests; namespace Bit.Core.Tools.ReportFeatures.Requests;
public class AddPasswordHealthReportApplicationRequest public class AddPasswordHealthReportApplicationRequest
{ {

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Tools.ReportFeatures.Requests;
public class DropPasswordHealthReportApplicationRequest
{
public Guid OrganizationId { get; set; }
public IEnumerable<Guid> PasswordHealthReportApplicationIds { get; set; }
}

View File

@ -1,7 +1,9 @@
using Bit.Api.Tools.Controllers; using AutoFixture;
using Bit.Api.Tools.Controllers;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@ -45,5 +47,98 @@ public class ReportsControllerTests
.Received(0); .Received(0);
} }
[Theory, BitAutoData]
public async Task AddPasswordHealthReportApplicationAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).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<IAddPasswordHealthReportApplicationCommand>()
.Received(1)
.AddPasswordHealthReportApplicationAsync(Arg.Is<AddPasswordHealthReportApplicationRequest>(_ =>
_.OrganizationId == request.OrganizationId && _.Url == request.Url));
}
[Theory, BitAutoData]
public async Task AddPasswordHealthReportApplicationAsync_multiple_withAccess_success(
SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var fixture = new Fixture();
var request = fixture.CreateMany<Api.Tools.Models.PasswordHealthReportApplicationModel>(2);
await sutProvider.Sut.AddPasswordHealthReportApplications(request);
// Assert
_ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()
.Received(1)
.AddPasswordHealthReportApplicationAsync(Arg.Any<IEnumerable<AddPasswordHealthReportApplicationRequest>>());
}
[Theory, BitAutoData]
public async Task AddPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel
{
OrganizationId = Guid.NewGuid(),
Url = "https://example.com",
};
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddPasswordHealthReportApplication(request));
// Assert
_ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var fixture = new Fixture();
var request = fixture.Create<Api.Tools.Models.PasswordHealthReportApplicationModel>();
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddPasswordHealthReportApplication(request));
// Assert
_ = sutProvider.GetDependency<IDropPasswordHealthReportApplicationCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var fixture = new Fixture();
var request = fixture.Create<DropPasswordHealthReportApplicationRequest>();
await sutProvider.Sut.DropPasswordHealthReportApplication(request);
// Assert
_ = sutProvider.GetDependency<IDropPasswordHealthReportApplicationCommand>()
.Received(1)
.DropPasswordHealthReportApplicationAsync(Arg.Is<DropPasswordHealthReportApplicationRequest>(_ =>
_.OrganizationId == request.OrganizationId &&
_.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds));
}
} }

View File

@ -4,8 +4,8 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Requests;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;

View File

@ -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<DropPasswordHealthReportApplicationCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var passwordHealthReportApplications = fixture.CreateMany<PasswordHealthReportApplication>(2).ToList();
// only take one id from the list - we only want to drop one record
var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()
.With(x => x.PasswordHealthReportApplicationIds,
passwordHealthReportApplications.Select(x => x.Id).Take(1).ToList())
.Create();
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(passwordHealthReportApplications);
// Act
await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request);
// Assert
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.DeleteAsync(Arg.Is<PasswordHealthReportApplication>(_ =>
request.PasswordHealthReportApplicationIds.Contains(_.Id)));
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_nothingToDrop(
SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var passwordHealthReportApplications = fixture.CreateMany<PasswordHealthReportApplication>(2).ToList();
// we are passing invalid data
var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()
.With(x => x.PasswordHealthReportApplicationIds, new List<Guid> { Guid.NewGuid() })
.Create();
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(passwordHealthReportApplications);
// Act
await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request);
// Assert
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(0)
.DeleteAsync(Arg.Any<PasswordHealthReportApplication>());
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withNodata_fails(
SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
// we are passing invalid data
var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()
.Create();
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(null as List<PasswordHealthReportApplication>);
// Act
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request));
// Assert
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(0)
.DeleteAsync(Arg.Any<PasswordHealthReportApplication>());
}
}