In progress
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled

This commit is contained in:
Yaojia Wang
2025-11-03 14:00:24 +01:00
parent fe8ad1c1f9
commit 1f66b25f30
74 changed files with 9609 additions and 28 deletions

View File

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

View File

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

View File

@@ -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>

View File

@@ -0,0 +1,10 @@
namespace ColaFlow.Modules.Identity.Domain.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@@ -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*");
}
}