Rui Tomé e7f3b6b12f
[PM-26430] Remove Type property from PolicyRequestModel to use route parameter only (#6472)
* Enhance PolicyRequestModel and SavePolicyRequest with validation for policy data and metadata.

* Add integration tests for policy updates to validate handling of invalid data types in PolicyRequestModel and SavePolicyRequest.

* Add missing using

* Update PolicyRequestModel for null safety by making Data and ValidateAndSerializePolicyData nullable

* Add integration tests for public PoliciesController to validate handling of invalid data types in policy updates.

* Add PolicyDataValidator class for validating and serializing policy data and metadata based on policy type.

* Refactor PolicyRequestModel, SavePolicyRequest, and PolicyUpdateRequestModel to utilize PolicyDataValidator for data validation and serialization, removing redundant methods and improving code clarity.

* Update PolicyRequestModel and SavePolicyRequest to initialize Data and Metadata properties with empty dictionaries.

* Refactor PolicyDataValidator to remove null checks for input data in validation methods

* Rename test methods in SavePolicyRequestTests to reflect handling of empty data and metadata, and remove null assignments in test cases for improved clarity.

* Remove Type property from PolicyRequestModel to use route parameter only

* Run dotnet format

* Enhance error handling in PolicyDataValidator to include field-specific details in BadRequestException messages.

* Enhance PoliciesControllerTests to verify error messages for BadRequest responses by checking for specific field names in the response content.

* refactor: Update PolicyRequestModel and SavePolicyRequest to use nullable dictionaries for Data and Metadata properties; enhance validation methods in PolicyDataValidator to handle null cases.

* test: Add integration tests for handling policies with null data in PoliciesController

* fix: Catch specific JsonException in PolicyDataValidator to improve error handling

* test: Add unit tests for PolicyDataValidator to validate and serialize policy data and metadata

* test: Remove PolicyType from PolicyRequestModel in PoliciesControllerTests

* test: Update PolicyDataValidatorTests to validate organization data ownership metadata

* Refactor PoliciesControllerTests to include policy type in PutVNext method calls
2025-11-10 15:27:44 +00:00

401 lines
13 KiB
C#

using System.Net;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
public PoliciesControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_factory.SubstituteService<Core.Services.IFeatureService>(featureService =>
{
featureService
.IsEnabled("pm-19467-create-default-location")
.Returns(true);
});
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
await _loginHelper.LoginAsync(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task PutVNext_OrganizationDataOwnershipPolicy_Success()
{
// Arrange
const PolicyType policyType = PolicyType.OrganizationDataOwnership;
const string defaultCollectionName = "Test Default Collection";
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Enabled = true,
},
Metadata = new Dictionary<string, object>
{
{ "defaultUserCollectionName", defaultCollectionName }
}
};
var (_, admin) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.User);
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
await AssertResponse();
await AssertPolicy();
await AssertDefaultCollectionCreatedOnlyForUserTypeAsync();
return;
async Task AssertDefaultCollectionCreatedOnlyForUserTypeAsync()
{
var collectionRepository = _factory.GetService<ICollectionRepository>();
await AssertUserExpectations(collectionRepository);
await AssertAdminExpectations(collectionRepository);
}
async Task AssertUserExpectations(ICollectionRepository collectionRepository)
{
var collections = await collectionRepository.GetManyByUserIdAsync(user.UserId!.Value);
var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName);
Assert.NotNull(defaultCollection);
Assert.Equal(_organization.Id, defaultCollection.OrganizationId);
}
async Task AssertAdminExpectations(ICollectionRepository collectionRepository)
{
var collections = await collectionRepository.GetManyByUserIdAsync(admin.UserId!.Value);
var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName);
Assert.Null(defaultCollection);
}
async Task AssertResponse()
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.True(content.Enabled);
Assert.Equal(policyType, content.Type);
Assert.Equal(_organization.Id, content.OrganizationId);
}
async Task AssertPolicy()
{
var policyRepository = _factory.GetService<IPolicyRepository>();
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
Assert.NotNull(policy);
Assert.True(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.Null(policy.Data);
Assert.Equal(_organization.Id, policy.OrganizationId);
}
}
[Fact]
public async Task PutVNext_MasterPasswordPolicy_Success()
{
// Arrange
var policyType = PolicyType.MasterPassword;
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 10 },
{ "minLength", 12 },
{ "requireUpper", true },
{ "requireLower", false },
{ "requireNumbers", true },
{ "requireSpecial", false },
{ "enforceOnLogin", true }
}
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
await AssertResponse();
await AssertPolicyDataForMasterPasswordPolicy();
return;
async Task AssertPolicyDataForMasterPasswordPolicy()
{
var policyRepository = _factory.GetService<IPolicyRepository>();
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
AssertPolicy(policy);
AssertMasterPasswordPolicyData(policy);
}
async Task AssertResponse()
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.True(content.Enabled);
Assert.Equal(policyType, content.Type);
Assert.Equal(_organization.Id, content.OrganizationId);
}
void AssertPolicy(Policy policy)
{
Assert.NotNull(policy);
Assert.True(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.Equal(_organization.Id, policy.OrganizationId);
Assert.NotNull(policy.Data);
}
void AssertMasterPasswordPolicyData(Policy policy)
{
var resultData = policy.GetDataModel<MasterPasswordPolicyData>();
var json = JsonSerializer.Serialize(request.Policy.Data);
var expectedData = JsonSerializer.Deserialize<MasterPasswordPolicyData>(json);
AssertHelper.AssertPropertyEqual(resultData, expectedData);
}
}
[Fact]
public async Task Put_MasterPasswordPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.MasterPassword;
var request = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minLength", "not a number" }, // Wrong type - should be int
{ "requireUpper", true }
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("minLength", content); // Verify field name is in error message
}
[Fact]
public async Task Put_SendOptionsPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.SendOptions;
var request = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "disableHideEmail", "not a boolean" } // Wrong type - should be bool
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task Put_ResetPasswordPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.ResetPassword;
var request = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "autoEnrollEnabled", 123 } // Wrong type - should be bool
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PutVNext_MasterPasswordPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.MasterPassword;
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", "not a number" }, // Wrong type - should be int
{ "minLength", 12 }
}
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("minComplexity", content); // Verify field name is in error message
}
[Fact]
public async Task PutVNext_SendOptionsPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.SendOptions;
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "disableHideEmail", "not a boolean" } // Wrong type - should be bool
}
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PutVNext_ResetPasswordPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.ResetPassword;
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "autoEnrollEnabled", 123 } // Wrong type - should be bool
}
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task Put_PolicyWithNullData_Success()
{
// Arrange
var policyType = PolicyType.SingleOrg;
var request = new PolicyRequestModel
{
Enabled = true,
Data = null
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task PutVNext_PolicyWithNullData_Success()
{
// Arrange
var policyType = PolicyType.TwoFactorAuthentication;
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Enabled = true,
Data = null
},
Metadata = null
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}