In progress
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Domain.Tests.Aggregates;
|
||||
|
||||
public sealed class TenantTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var name = TenantName.Create("Acme Corporation");
|
||||
var slug = TenantSlug.Create("acme");
|
||||
|
||||
// Act
|
||||
var tenant = Tenant.Create(name, slug);
|
||||
|
||||
// Assert
|
||||
tenant.Should().NotBeNull();
|
||||
tenant.Id.Should().NotBe(Guid.Empty);
|
||||
tenant.Name.Value.Should().Be("Acme Corporation");
|
||||
tenant.Slug.Value.Should().Be("acme");
|
||||
tenant.Status.Should().Be(TenantStatus.Active);
|
||||
tenant.Plan.Should().Be(SubscriptionPlan.Free);
|
||||
tenant.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ShouldRaiseTenantCreatedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var name = TenantName.Create("Acme Corporation");
|
||||
var slug = TenantSlug.Create("acme");
|
||||
|
||||
// Act
|
||||
var tenant = Tenant.Create(name, slug);
|
||||
|
||||
// Assert
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<TenantCreatedEvent>();
|
||||
var domainEvent = tenant.DomainEvents.First() as TenantCreatedEvent;
|
||||
domainEvent.Should().NotBeNull();
|
||||
domainEvent!.TenantId.Should().Be(tenant.Id);
|
||||
domainEvent.Slug.Should().Be("acme");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_ShouldChangeStatus()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.Suspend("Payment failed");
|
||||
tenant.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
tenant.Activate();
|
||||
|
||||
// Assert
|
||||
tenant.Status.Should().Be(TenantStatus.Active);
|
||||
tenant.SuspendedAt.Should().BeNull();
|
||||
tenant.SuspensionReason.Should().BeNull();
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<TenantActivatedEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Suspend_ShouldChangeStatus()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
tenant.Suspend("Payment failed");
|
||||
|
||||
// Assert
|
||||
tenant.Status.Should().Be(TenantStatus.Suspended);
|
||||
tenant.SuspendedAt.Should().NotBeNull();
|
||||
tenant.SuspendedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
tenant.SuspensionReason.Should().Be("Payment failed");
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<TenantSuspendedEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cancel_ShouldChangeStatus()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
tenant.Cancel();
|
||||
|
||||
// Assert
|
||||
tenant.Status.Should().Be(TenantStatus.Cancelled);
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<TenantCancelledEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelledTenant_CannotBeActivated()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => tenant.Activate();
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Cannot activate cancelled tenant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureSso_ShouldSucceed_ForProfessionalPlan()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.UpgradePlan(SubscriptionPlan.Professional);
|
||||
tenant.ClearDomainEvents();
|
||||
|
||||
var ssoConfig = SsoConfiguration.CreateOidc(
|
||||
SsoProvider.AzureAD,
|
||||
"https://login.microsoftonline.com/tenant-id",
|
||||
"client-id",
|
||||
"client-secret");
|
||||
|
||||
// Act
|
||||
tenant.ConfigureSso(ssoConfig);
|
||||
|
||||
// Assert
|
||||
tenant.SsoConfig.Should().NotBeNull();
|
||||
tenant.SsoConfig!.Provider.Should().Be(SsoProvider.AzureAD);
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<SsoConfiguredEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureSso_ShouldThrow_ForFreePlan()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
var ssoConfig = SsoConfiguration.CreateOidc(
|
||||
SsoProvider.AzureAD,
|
||||
"https://login.microsoftonline.com/tenant-id",
|
||||
"client-id",
|
||||
"client-secret");
|
||||
|
||||
// Act & Assert
|
||||
var act = () => tenant.ConfigureSso(ssoConfig);
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("SSO is only available for Professional and Enterprise plans");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpgradePlan_ShouldIncreaseResourceLimits()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
tenant.UpgradePlan(SubscriptionPlan.Professional);
|
||||
|
||||
// Assert
|
||||
tenant.Plan.Should().Be(SubscriptionPlan.Professional);
|
||||
tenant.MaxUsers.Should().Be(100);
|
||||
tenant.MaxProjects.Should().Be(100);
|
||||
tenant.MaxStorageGB.Should().Be(100);
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<TenantPlanUpgradedEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpgradePlan_ShouldThrow_WhenDowngrading()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"), SubscriptionPlan.Professional);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => tenant.UpgradePlan(SubscriptionPlan.Free);
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("New plan must be higher than current plan");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisableSso_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(TenantName.Create("Test"), TenantSlug.Create("test"));
|
||||
tenant.UpgradePlan(SubscriptionPlan.Enterprise);
|
||||
var ssoConfig = SsoConfiguration.CreateOidc(
|
||||
SsoProvider.Google,
|
||||
"https://accounts.google.com",
|
||||
"client-id",
|
||||
"client-secret");
|
||||
tenant.ConfigureSso(ssoConfig);
|
||||
tenant.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
tenant.DisableSso();
|
||||
|
||||
// Assert
|
||||
tenant.SsoConfig.Should().BeNull();
|
||||
tenant.DomainEvents.Should().ContainSingle();
|
||||
tenant.DomainEvents.Should().ContainItemsAssignableTo<SsoDisabledEvent>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Domain.Tests.Aggregates;
|
||||
|
||||
public sealed class UserTests
|
||||
{
|
||||
private readonly TenantId _tenantId = TenantId.CreateUnique();
|
||||
|
||||
[Fact]
|
||||
public void CreateLocal_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var email = Email.Create("test@example.com");
|
||||
var fullName = FullName.Create("John Doe");
|
||||
var passwordHash = "hashed_password";
|
||||
|
||||
// Act
|
||||
var user = User.CreateLocal(_tenantId, email, passwordHash, fullName);
|
||||
|
||||
// Assert
|
||||
user.Should().NotBeNull();
|
||||
user.Id.Should().NotBe(Guid.Empty);
|
||||
user.TenantId.Should().Be(_tenantId);
|
||||
user.Email.Value.Should().Be("test@example.com");
|
||||
user.FullName.Value.Should().Be("John Doe");
|
||||
user.PasswordHash.Should().Be(passwordHash);
|
||||
user.Status.Should().Be(UserStatus.Active);
|
||||
user.AuthProvider.Should().Be(AuthenticationProvider.Local);
|
||||
user.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateLocal_ShouldRaiseUserCreatedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var email = Email.Create("test@example.com");
|
||||
var fullName = FullName.Create("John Doe");
|
||||
|
||||
// Act
|
||||
var user = User.CreateLocal(_tenantId, email, "password", fullName);
|
||||
|
||||
// Assert
|
||||
user.DomainEvents.Should().ContainSingle();
|
||||
user.DomainEvents.Should().ContainItemsAssignableTo<UserCreatedEvent>();
|
||||
var domainEvent = user.DomainEvents.First() as UserCreatedEvent;
|
||||
domainEvent.Should().NotBeNull();
|
||||
domainEvent!.UserId.Should().Be(user.Id);
|
||||
domainEvent.Email.Should().Be("test@example.com");
|
||||
domainEvent.TenantId.Should().Be(_tenantId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromSso_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var email = Email.Create("user@company.com");
|
||||
var fullName = FullName.Create("Jane Smith");
|
||||
var externalUserId = "google-12345";
|
||||
var avatarUrl = "https://example.com/avatar.jpg";
|
||||
|
||||
// Act
|
||||
var user = User.CreateFromSso(
|
||||
_tenantId,
|
||||
AuthenticationProvider.Google,
|
||||
externalUserId,
|
||||
email,
|
||||
fullName,
|
||||
avatarUrl);
|
||||
|
||||
// Assert
|
||||
user.Should().NotBeNull();
|
||||
user.TenantId.Should().Be(_tenantId);
|
||||
user.AuthProvider.Should().Be(AuthenticationProvider.Google);
|
||||
user.ExternalUserId.Should().Be(externalUserId);
|
||||
user.ExternalEmail.Should().Be("user@company.com");
|
||||
user.AvatarUrl.Should().Be(avatarUrl);
|
||||
user.PasswordHash.Should().BeEmpty();
|
||||
user.EmailVerifiedAt.Should().NotBeNull(); // SSO users are auto-verified
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromSso_ShouldRaiseUserCreatedFromSsoEvent()
|
||||
{
|
||||
// Arrange
|
||||
var email = Email.Create("user@company.com");
|
||||
var fullName = FullName.Create("Jane Smith");
|
||||
|
||||
// Act
|
||||
var user = User.CreateFromSso(
|
||||
_tenantId,
|
||||
AuthenticationProvider.AzureAD,
|
||||
"azure-123",
|
||||
email,
|
||||
fullName);
|
||||
|
||||
// Assert
|
||||
user.DomainEvents.Should().ContainSingle();
|
||||
user.DomainEvents.Should().ContainItemsAssignableTo<UserCreatedFromSsoEvent>();
|
||||
var domainEvent = user.DomainEvents.First() as UserCreatedFromSsoEvent;
|
||||
domainEvent.Should().NotBeNull();
|
||||
domainEvent!.Provider.Should().Be(AuthenticationProvider.AzureAD);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromSso_ShouldThrow_ForLocalProvider()
|
||||
{
|
||||
// Arrange
|
||||
var email = Email.Create("test@example.com");
|
||||
var fullName = FullName.Create("John Doe");
|
||||
|
||||
// Act & Assert
|
||||
var act = () => User.CreateFromSso(
|
||||
_tenantId,
|
||||
AuthenticationProvider.Local,
|
||||
"external-id",
|
||||
email,
|
||||
fullName);
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("Use CreateLocal for local authentication");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePassword_ShouldSucceed_ForLocalUser()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"old_hash",
|
||||
FullName.Create("John Doe"));
|
||||
user.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
user.UpdatePassword("new_hash");
|
||||
|
||||
// Assert
|
||||
user.PasswordHash.Should().Be("new_hash");
|
||||
user.DomainEvents.Should().ContainSingle();
|
||||
user.DomainEvents.Should().ContainItemsAssignableTo<UserPasswordChangedEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePassword_ShouldThrow_ForSsoUser()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateFromSso(
|
||||
_tenantId,
|
||||
AuthenticationProvider.Google,
|
||||
"google-123",
|
||||
Email.Create("test@example.com"),
|
||||
FullName.Create("John Doe"));
|
||||
|
||||
// Act & Assert
|
||||
var act = () => user.UpdatePassword("new_hash");
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Cannot change password for SSO users");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateProfile_ShouldUpdateAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"hash",
|
||||
FullName.Create("John Doe"));
|
||||
|
||||
// Act
|
||||
user.UpdateProfile(
|
||||
fullName: FullName.Create("Jane Smith"),
|
||||
avatarUrl: "https://example.com/avatar.jpg",
|
||||
jobTitle: "Software Engineer",
|
||||
phoneNumber: "+1234567890");
|
||||
|
||||
// Assert
|
||||
user.FullName.Value.Should().Be("Jane Smith");
|
||||
user.AvatarUrl.Should().Be("https://example.com/avatar.jpg");
|
||||
user.JobTitle.Should().Be("Software Engineer");
|
||||
user.PhoneNumber.Should().Be("+1234567890");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordLogin_ShouldUpdateLastLoginAt()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"hash",
|
||||
FullName.Create("John Doe"));
|
||||
|
||||
// Act
|
||||
user.RecordLogin();
|
||||
|
||||
// Assert
|
||||
user.LastLoginAt.Should().NotBeNull();
|
||||
user.LastLoginAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyEmail_ShouldSetEmailVerifiedAt()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"hash",
|
||||
FullName.Create("John Doe"));
|
||||
|
||||
// Act
|
||||
user.VerifyEmail();
|
||||
|
||||
// Assert
|
||||
user.EmailVerifiedAt.Should().NotBeNull();
|
||||
user.EmailVerifiedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
user.EmailVerificationToken.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Suspend_ShouldChangeStatus()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"hash",
|
||||
FullName.Create("John Doe"));
|
||||
user.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
user.Suspend("Violation of terms");
|
||||
|
||||
// Assert
|
||||
user.Status.Should().Be(UserStatus.Suspended);
|
||||
user.DomainEvents.Should().ContainSingle();
|
||||
user.DomainEvents.Should().ContainItemsAssignableTo<UserSuspendedEvent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reactivate_ShouldChangeStatusToActive()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"hash",
|
||||
FullName.Create("John Doe"));
|
||||
user.Suspend("Test");
|
||||
|
||||
// Act
|
||||
user.Reactivate();
|
||||
|
||||
// Assert
|
||||
user.Status.Should().Be(UserStatus.Active);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSsoProfile_ShouldSucceed_ForSsoUser()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateFromSso(
|
||||
_tenantId,
|
||||
AuthenticationProvider.Google,
|
||||
"google-123",
|
||||
Email.Create("old@example.com"),
|
||||
FullName.Create("Old Name"));
|
||||
|
||||
// Act
|
||||
user.UpdateSsoProfile(
|
||||
"google-456",
|
||||
Email.Create("new@example.com"),
|
||||
FullName.Create("New Name"),
|
||||
"https://new-avatar.jpg");
|
||||
|
||||
// Assert
|
||||
user.ExternalUserId.Should().Be("google-456");
|
||||
user.ExternalEmail.Should().Be("new@example.com");
|
||||
user.FullName.Value.Should().Be("New Name");
|
||||
user.AvatarUrl.Should().Be("https://new-avatar.jpg");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSsoProfile_ShouldThrow_ForLocalUser()
|
||||
{
|
||||
// Arrange
|
||||
var user = User.CreateLocal(
|
||||
_tenantId,
|
||||
Email.Create("test@example.com"),
|
||||
"hash",
|
||||
FullName.Create("John Doe"));
|
||||
|
||||
// Act & Assert
|
||||
var act = () => user.UpdateSsoProfile(
|
||||
"external-id",
|
||||
Email.Create("new@example.com"),
|
||||
FullName.Create("New Name"));
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Cannot update SSO profile for local users");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Modules\Identity\ColaFlow.Modules.Identity.Domain\ColaFlow.Modules.Identity.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.Modules.Identity.Domain.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Domain.Tests.ValueObjects;
|
||||
|
||||
public sealed class TenantSlugTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("acme")]
|
||||
[InlineData("beta-corp")]
|
||||
[InlineData("test-123")]
|
||||
[InlineData("abc")]
|
||||
public void Create_ShouldSucceed_ForValidSlug(string slug)
|
||||
{
|
||||
// Act
|
||||
var tenantSlug = TenantSlug.Create(slug);
|
||||
|
||||
// Assert
|
||||
tenantSlug.Value.Should().Be(slug);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("ab")] // Too short
|
||||
[InlineData("www")] // Reserved
|
||||
[InlineData("api")] // Reserved
|
||||
[InlineData("admin")] // Reserved
|
||||
[InlineData("acme_corp")] // Underscore
|
||||
[InlineData("acme corp")] // Space
|
||||
[InlineData("-acme")] // Starts with hyphen
|
||||
[InlineData("acme-")] // Ends with hyphen
|
||||
[InlineData("acme--corp")] // Double hyphen
|
||||
public void Create_ShouldThrow_ForInvalidSlug(string slug)
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => TenantSlug.Create(slug);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ShouldConvertToLowerCase()
|
||||
{
|
||||
// Act
|
||||
var tenantSlug = TenantSlug.Create("AcmeCorp");
|
||||
|
||||
// Assert
|
||||
tenantSlug.Value.Should().Be("acmecorp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ShouldTrimWhitespace()
|
||||
{
|
||||
// Act
|
||||
var tenantSlug = TenantSlug.Create(" acme ");
|
||||
|
||||
// Assert
|
||||
tenantSlug.Value.Should().Be("acme");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ShouldThrow_ForTooLongSlug()
|
||||
{
|
||||
// Arrange
|
||||
var longSlug = new string('a', 51);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => TenantSlug.Create(longSlug);
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*cannot exceed 50 characters*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Modules\Identity\ColaFlow.Modules.Identity.Infrastructure\ColaFlow.Modules.Identity.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,171 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
||||
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.Identity.Infrastructure.Services;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Infrastructure.Tests.Persistence;
|
||||
|
||||
public class GlobalQueryFilterTests : IDisposable
|
||||
{
|
||||
private readonly Mock<ITenantContext> _mockTenantContext;
|
||||
private readonly IdentityDbContext _context;
|
||||
|
||||
public GlobalQueryFilterTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
_mockTenantContext = new Mock<ITenantContext>();
|
||||
_context = new IdentityDbContext(options, _mockTenantContext.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GlobalQueryFilter_ShouldFilterByTenant()
|
||||
{
|
||||
// Arrange - Create 2 users from different tenants
|
||||
var tenant1Id = TenantId.CreateUnique();
|
||||
var tenant2Id = TenantId.CreateUnique();
|
||||
|
||||
// Setup mock to filter for tenant1
|
||||
var mockTenantContext = new Mock<ITenantContext>();
|
||||
mockTenantContext.Setup(x => x.IsSet).Returns(true);
|
||||
mockTenantContext.Setup(x => x.TenantId).Returns(tenant1Id);
|
||||
|
||||
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
using var context = new IdentityDbContext(options, mockTenantContext.Object);
|
||||
|
||||
var user1 = User.CreateLocal(
|
||||
tenant1Id,
|
||||
Email.Create("user1@tenant1.com"),
|
||||
"password123",
|
||||
FullName.Create("User One"));
|
||||
|
||||
var user2 = User.CreateLocal(
|
||||
tenant2Id,
|
||||
Email.Create("user2@tenant2.com"),
|
||||
"password123",
|
||||
FullName.Create("User Two"));
|
||||
|
||||
await context.Users.AddAsync(user1);
|
||||
await context.Users.AddAsync(user2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act - Query users (should be filtered by tenant1)
|
||||
var filteredUsers = await context.Users.ToListAsync();
|
||||
|
||||
// Assert - Should only return tenant1's user
|
||||
filteredUsers.Should().HaveCount(1);
|
||||
filteredUsers[0].Email.Value.Should().Be("user1@tenant1.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithoutTenantFilter_ShouldReturnAllUsers()
|
||||
{
|
||||
// Arrange - Create 2 users from different tenants
|
||||
var tenant1Id = TenantId.CreateUnique();
|
||||
var tenant2Id = TenantId.CreateUnique();
|
||||
|
||||
// Setup mock to filter for tenant1
|
||||
var mockTenantContext = new Mock<ITenantContext>();
|
||||
mockTenantContext.Setup(x => x.IsSet).Returns(true);
|
||||
mockTenantContext.Setup(x => x.TenantId).Returns(tenant1Id);
|
||||
|
||||
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
using var context = new IdentityDbContext(options, mockTenantContext.Object);
|
||||
|
||||
var user1 = User.CreateLocal(tenant1Id, Email.Create("admin@tenant1.com"), "pass", FullName.Create("Admin One"));
|
||||
var user2 = User.CreateLocal(tenant2Id, Email.Create("admin@tenant2.com"), "pass", FullName.Create("Admin Two"));
|
||||
|
||||
await context.Users.AddAsync(user1);
|
||||
await context.Users.AddAsync(user2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act - Use WithoutTenantFilter to bypass filter
|
||||
var allUsers = await context.WithoutTenantFilter<User>().ToListAsync();
|
||||
|
||||
// Assert - Should return all users
|
||||
allUsers.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GlobalQueryFilter_ShouldNotFilter_WhenTenantContextNotSet()
|
||||
{
|
||||
// Arrange - Tenant context not set
|
||||
var tenant1Id = TenantId.CreateUnique();
|
||||
var tenant2Id = TenantId.CreateUnique();
|
||||
|
||||
var mockTenantContext = new Mock<ITenantContext>();
|
||||
mockTenantContext.Setup(x => x.IsSet).Returns(false); // NOT set
|
||||
|
||||
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
using var context = new IdentityDbContext(options, mockTenantContext.Object);
|
||||
|
||||
var user1 = User.CreateLocal(tenant1Id, Email.Create("user1@test.com"), "pass", FullName.Create("User One"));
|
||||
var user2 = User.CreateLocal(tenant2Id, Email.Create("user2@test.com"), "pass", FullName.Create("User Two"));
|
||||
|
||||
await context.Users.AddAsync(user1);
|
||||
await context.Users.AddAsync(user2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act - Query without tenant filter (because IsSet = false)
|
||||
var allUsers = await context.Users.ToListAsync();
|
||||
|
||||
// Assert - Should return all users (no filtering)
|
||||
allUsers.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UserRepository_ShouldRespectTenantFilter()
|
||||
{
|
||||
// Arrange
|
||||
var tenant1Id = TenantId.CreateUnique();
|
||||
var tenant2Id = TenantId.CreateUnique();
|
||||
|
||||
var mockTenantContext = new Mock<ITenantContext>();
|
||||
mockTenantContext.Setup(x => x.IsSet).Returns(true);
|
||||
mockTenantContext.Setup(x => x.TenantId).Returns(tenant1Id);
|
||||
|
||||
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
using var context = new IdentityDbContext(options, mockTenantContext.Object);
|
||||
|
||||
var user1 = User.CreateLocal(tenant1Id, Email.Create("john@tenant1.com"), "pass", FullName.Create("John Doe"));
|
||||
var user2 = User.CreateLocal(tenant2Id, Email.Create("jane@tenant2.com"), "pass", FullName.Create("Jane Doe"));
|
||||
|
||||
await context.Users.AddAsync(user1);
|
||||
await context.Users.AddAsync(user2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var repository = new ColaFlow.Modules.Identity.Infrastructure.Persistence.Repositories.UserRepository(context);
|
||||
var retrievedUser = await repository.GetByIdAsync(UserId.Create(user1.Id));
|
||||
|
||||
// Assert - Should find user1 (same tenant)
|
||||
retrievedUser.Should().NotBeNull();
|
||||
retrievedUser!.Email.Value.Should().Be("john@tenant1.com");
|
||||
|
||||
// Trying to get user2 (from different tenant) should return null due to filter
|
||||
var user2Attempt = await repository.GetByIdAsync(UserId.Create(user2.Id));
|
||||
user2Attempt.Should().BeNull();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.Identity.Infrastructure.Persistence.Repositories;
|
||||
using ColaFlow.Modules.Identity.Infrastructure.Services;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Infrastructure.Tests.Repositories;
|
||||
|
||||
public class TenantRepositoryTests : IDisposable
|
||||
{
|
||||
private readonly IdentityDbContext _context;
|
||||
private readonly TenantRepository _repository;
|
||||
|
||||
public TenantRepositoryTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
var mockTenantContext = new Mock<ITenantContext>();
|
||||
mockTenantContext.Setup(x => x.IsSet).Returns(false);
|
||||
|
||||
_context = new IdentityDbContext(options, mockTenantContext.Object);
|
||||
_repository = new TenantRepository(_context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAsync_ShouldPersistTenant()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(
|
||||
TenantName.Create("Test Company"),
|
||||
TenantSlug.Create("test-company"),
|
||||
SubscriptionPlan.Professional);
|
||||
|
||||
// Act
|
||||
await _repository.AddAsync(tenant);
|
||||
|
||||
// Assert
|
||||
var retrieved = await _repository.GetByIdAsync(TenantId.Create(tenant.Id));
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.Name.Value.Should().Be("Test Company");
|
||||
retrieved.Slug.Value.Should().Be("test-company");
|
||||
retrieved.Plan.Should().Be(SubscriptionPlan.Professional);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBySlugAsync_ShouldReturnTenant()
|
||||
{
|
||||
// Arrange
|
||||
var slug = TenantSlug.Create("acme-corp");
|
||||
var tenant = Tenant.Create(
|
||||
TenantName.Create("Acme Corp"),
|
||||
slug,
|
||||
SubscriptionPlan.Enterprise);
|
||||
await _repository.AddAsync(tenant);
|
||||
|
||||
// Act
|
||||
var retrieved = await _repository.GetBySlugAsync(slug);
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.Slug.Value.Should().Be("acme-corp");
|
||||
retrieved.Name.Value.Should().Be("Acme Corp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExistsBySlugAsync_ShouldReturnTrue_WhenSlugExists()
|
||||
{
|
||||
// Arrange
|
||||
var slug = TenantSlug.Create("unique-slug");
|
||||
var tenant = Tenant.Create(
|
||||
TenantName.Create("Unique Company"),
|
||||
slug,
|
||||
SubscriptionPlan.Free);
|
||||
await _repository.AddAsync(tenant);
|
||||
|
||||
// Act
|
||||
var exists = await _repository.ExistsBySlugAsync(slug);
|
||||
|
||||
// Assert
|
||||
exists.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExistsBySlugAsync_ShouldReturnFalse_WhenSlugDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var slug = TenantSlug.Create("non-existent");
|
||||
|
||||
// Act
|
||||
var exists = await _repository.ExistsBySlugAsync(slug);
|
||||
|
||||
// Assert
|
||||
exists.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_ShouldModifyTenant()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(
|
||||
TenantName.Create("Original Name"),
|
||||
TenantSlug.Create("original-slug"),
|
||||
SubscriptionPlan.Free);
|
||||
await _repository.AddAsync(tenant);
|
||||
|
||||
// Act
|
||||
tenant.UpdateName(TenantName.Create("Updated Name"));
|
||||
await _repository.UpdateAsync(tenant);
|
||||
|
||||
// Assert
|
||||
var retrieved = await _repository.GetByIdAsync(TenantId.Create(tenant.Id));
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.Name.Value.Should().Be("Updated Name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllAsync_ShouldReturnAllTenants()
|
||||
{
|
||||
// Arrange
|
||||
var tenant1 = Tenant.Create(TenantName.Create("Tenant 1"), TenantSlug.Create("tenant-1"), SubscriptionPlan.Free);
|
||||
var tenant2 = Tenant.Create(TenantName.Create("Tenant 2"), TenantSlug.Create("tenant-2"), SubscriptionPlan.Starter);
|
||||
await _repository.AddAsync(tenant1);
|
||||
await _repository.AddAsync(tenant2);
|
||||
|
||||
// Act
|
||||
var allTenants = await _repository.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
allTenants.Should().HaveCount(2);
|
||||
allTenants.Should().Contain(t => t.Name.Value == "Tenant 1");
|
||||
allTenants.Should().Contain(t => t.Name.Value == "Tenant 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ShouldRemoveTenant()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = Tenant.Create(
|
||||
TenantName.Create("To Delete"),
|
||||
TenantSlug.Create("to-delete"),
|
||||
SubscriptionPlan.Free);
|
||||
await _repository.AddAsync(tenant);
|
||||
|
||||
// Act
|
||||
await _repository.DeleteAsync(tenant);
|
||||
|
||||
// Assert
|
||||
var retrieved = await _repository.GetByIdAsync(TenantId.Create(tenant.Id));
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.Modules.Identity.Infrastructure.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user