using System.ComponentModel.DataAnnotations; using Bit.Api.Auth.Models.Request.Organizations; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Services; using Bit.Core.Sso; using Microsoft.Extensions.Localization; using NSubstitute; using Xunit; namespace Bit.Api.Test.Auth.Models.Request; public class OrganizationSsoRequestModelTests { [Fact] public void ToSsoConfig_WithOrganizationId_CreatesNewSsoConfig() { // Arrange var organizationId = Guid.NewGuid(); var model = new OrganizationSsoRequestModel { Enabled = true, Identifier = "test-identifier", Data = new SsoConfigurationDataRequest { ConfigType = SsoType.OpenIdConnect, Authority = "https://example.com", ClientId = "test-client", ClientSecret = "test-secret" } }; // Act var result = model.ToSsoConfig(organizationId); // Assert Assert.NotNull(result); Assert.Equal(organizationId, result.OrganizationId); Assert.True(result.Enabled); } [Fact] public void ToSsoConfig_WithExistingConfig_UpdatesExistingConfig() { // Arrange var organizationId = Guid.NewGuid(); var existingConfig = new SsoConfig { Id = 1, OrganizationId = organizationId, Enabled = false }; var model = new OrganizationSsoRequestModel { Enabled = true, Identifier = "updated-identifier", Data = new SsoConfigurationDataRequest { ConfigType = SsoType.Saml2, IdpEntityId = "test-entity", IdpSingleSignOnServiceUrl = "https://sso.example.com" } }; // Act var result = model.ToSsoConfig(existingConfig); // Assert Assert.Same(existingConfig, result); Assert.Equal(organizationId, result.OrganizationId); Assert.True(result.Enabled); } } public class SsoConfigurationDataRequestTests { private readonly TestI18nService _i18nService; private readonly ValidationContext _validationContext; public SsoConfigurationDataRequestTests() { _i18nService = new TestI18nService(); var serviceProvider = Substitute.For(); serviceProvider.GetService(typeof(II18nService)).Returns(_i18nService); _validationContext = new ValidationContext(new object(), serviceProvider, null); } [Fact] public void ToConfigurationData_MapsProperties() { // Arrange var model = new SsoConfigurationDataRequest { ConfigType = SsoType.OpenIdConnect, MemberDecryptionType = MemberDecryptionType.KeyConnector, Authority = "https://authority.example.com", ClientId = "test-client-id", ClientSecret = "test-client-secret", IdpX509PublicCert = "-----BEGIN CERTIFICATE-----\nMIIC...test\n-----END CERTIFICATE-----", SpOutboundSigningAlgorithm = null // Test default }; // Act var result = model.ToConfigurationData(); // Assert Assert.Equal(SsoType.OpenIdConnect, result.ConfigType); Assert.Equal(MemberDecryptionType.KeyConnector, result.MemberDecryptionType); Assert.Equal("https://authority.example.com", result.Authority); Assert.Equal("test-client-id", result.ClientId); Assert.Equal("test-client-secret", result.ClientSecret); Assert.Equal("MIIC...test", result.IdpX509PublicCert); // PEM headers stripped Assert.Equal(SamlSigningAlgorithms.Sha256, result.SpOutboundSigningAlgorithm); // Default applied Assert.Null(result.IdpArtifactResolutionServiceUrl); // Always null } [Fact] public void KeyConnectorEnabled_Setter_UpdatesMemberDecryptionType() { // Arrange var model = new SsoConfigurationDataRequest(); // Act & Assert #pragma warning disable CS0618 // Type or member is obsolete model.KeyConnectorEnabled = true; Assert.Equal(MemberDecryptionType.KeyConnector, model.MemberDecryptionType); model.KeyConnectorEnabled = false; Assert.Equal(MemberDecryptionType.MasterPassword, model.MemberDecryptionType); #pragma warning restore CS0618 // Type or member is obsolete } // Validation Tests [Fact] public void Validate_OpenIdConnect_ValidData_NoErrors() { // Arrange var model = new SsoConfigurationDataRequest { ConfigType = SsoType.OpenIdConnect, Authority = "https://example.com", ClientId = "test-client", ClientSecret = "test-secret" }; // Act var results = model.Validate(_validationContext).ToList(); // Assert Assert.Empty(results); } [Theory] [InlineData("", "test-client", "test-secret", "AuthorityValidationError")] [InlineData("https://example.com", "", "test-secret", "ClientIdValidationError")] [InlineData("https://example.com", "test-client", "", "ClientSecretValidationError")] public void Validate_OpenIdConnect_MissingRequiredFields_ReturnsErrors(string authority, string clientId, string clientSecret, string expectedError) { // Arrange var model = new SsoConfigurationDataRequest { ConfigType = SsoType.OpenIdConnect, Authority = authority, ClientId = clientId, ClientSecret = clientSecret }; // Act var results = model.Validate(_validationContext).ToList(); // Assert Assert.Single(results); Assert.Equal(expectedError, results[0].ErrorMessage); } [Fact] public void Validate_Saml2_ValidData_NoErrors() { // Arrange var model = new SsoConfigurationDataRequest { ConfigType = SsoType.Saml2, IdpEntityId = "https://idp.example.com", IdpSingleSignOnServiceUrl = "https://sso.example.com", IdpSingleLogoutServiceUrl = "https://logout.example.com" }; // Act var results = model.Validate(_validationContext).ToList(); // Assert Assert.Empty(results); } [Theory] [InlineData("", "https://sso.example.com", "IdpEntityIdValidationError")] [InlineData("not-a-valid-uri", "", "IdpSingleSignOnServiceUrlValidationError")] public void Validate_Saml2_MissingRequiredFields_ReturnsErrors(string entityId, string signOnUrl, string expectedError) { // Arrange var model = new SsoConfigurationDataRequest { ConfigType = SsoType.Saml2, IdpEntityId = entityId, IdpSingleSignOnServiceUrl = signOnUrl }; // Act var results = model.Validate(_validationContext).ToList(); // Assert Assert.Contains(results, r => r.ErrorMessage == expectedError); } [Theory] [InlineData("not-a-url")] [InlineData("ftp://example.com")] [InlineData("https://example.com