Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- xUnit Test Framework -->
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Test Utilities -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
|
||||
<!-- MediatR Testing -->
|
||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||
|
||||
<!-- Code Coverage -->
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Project References -->
|
||||
<ProjectReference Include="..\..\src\ColaFlow.Domain\ColaFlow.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\ColaFlow.Application\ColaFlow.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
<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="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\ColaFlow.Application\ColaFlow.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
10
colaflow-api/tests/ColaFlow.Application.Tests/UnitTest1.cs
Normal file
10
colaflow-api/tests/ColaFlow.Application.Tests/UnitTest1.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.Application.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NetArchTest.Rules" Version="1.3.2" />
|
||||
<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\ProjectManagement\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,175 @@
|
||||
using NetArchTest.Rules;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.ArchitectureTests;
|
||||
|
||||
/// <summary>
|
||||
/// Architecture tests to enforce module boundaries and dependencies.
|
||||
/// These tests ensure the modular monolith architecture is maintained.
|
||||
/// </summary>
|
||||
public class ModuleBoundaryTests
|
||||
{
|
||||
private const string PM_DOMAIN = "ColaFlow.Modules.ProjectManagement.Domain";
|
||||
private const string PM_APPLICATION = "ColaFlow.Modules.ProjectManagement.Application";
|
||||
private const string PM_INFRASTRUCTURE = "ColaFlow.Modules.ProjectManagement.Infrastructure";
|
||||
private const string SHARED_KERNEL = "ColaFlow.Shared.Kernel";
|
||||
|
||||
[Fact]
|
||||
public void Domain_Should_Not_Depend_On_Application()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var result = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.ResideInNamespace(PM_DOMAIN)
|
||||
.ShouldNot()
|
||||
.HaveDependencyOn(PM_APPLICATION)
|
||||
.GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccessful,
|
||||
$"Domain layer should not depend on Application layer. Violations: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Domain_Should_Not_Depend_On_Infrastructure()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var result = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.ResideInNamespace(PM_DOMAIN)
|
||||
.ShouldNot()
|
||||
.HaveDependencyOn(PM_INFRASTRUCTURE)
|
||||
.GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccessful,
|
||||
$"Domain layer should not depend on Infrastructure layer. Violations: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Domain_Can_Only_Depend_On_Shared_Kernel()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var result = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.ResideInNamespace(PM_DOMAIN)
|
||||
.And()
|
||||
.HaveDependencyOnAny("ColaFlow")
|
||||
.Should()
|
||||
.HaveDependencyOn(SHARED_KERNEL)
|
||||
.Or()
|
||||
.ResideInNamespace(PM_DOMAIN)
|
||||
.GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccessful,
|
||||
$"Domain layer should only depend on Shared.Kernel. Violations: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Application_Should_Not_Depend_On_Infrastructure()
|
||||
{
|
||||
// Arrange
|
||||
// Note: Application assembly might not have types yet, so we'll skip this test for now
|
||||
// This test will be enabled when Application layer is implemented
|
||||
|
||||
Assert.True(true, "Skipped - Application layer not yet implemented");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_Should_Be_AggregateRoot()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var result = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.HaveName("Project")
|
||||
.Should()
|
||||
.Inherit(typeof(AggregateRoot))
|
||||
.GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccessful,
|
||||
$"Project should inherit from AggregateRoot. Violations: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Entities_Should_Inherit_From_Entity()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var result = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.ResideInNamespace($"{PM_DOMAIN}.Aggregates")
|
||||
.And()
|
||||
.AreClasses()
|
||||
.And()
|
||||
.AreNotAbstract()
|
||||
.Should()
|
||||
.Inherit(typeof(Entity))
|
||||
.GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccessful,
|
||||
$"All entity classes should inherit from Entity. Violations: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Domain_Events_Should_Be_Records()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var types = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.ResideInNamespace($"{PM_DOMAIN}.Events")
|
||||
.And()
|
||||
.DoNotHaveName("DomainEvent") // Exclude base class
|
||||
.GetTypes();
|
||||
|
||||
// Assert - Check if types are records (records are sealed and inherit from a specific base)
|
||||
foreach (var type in types)
|
||||
{
|
||||
Assert.True(type.IsClass && type.IsSealed,
|
||||
$"Event {type.Name} should be a record (sealed class)");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueObjects_Should_Be_Immutable()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(Project).Assembly;
|
||||
|
||||
// Act
|
||||
var result = Types.InAssembly(assembly)
|
||||
.That()
|
||||
.ResideInNamespace($"{PM_DOMAIN}.ValueObjects")
|
||||
.And()
|
||||
.AreClasses()
|
||||
.And()
|
||||
.AreNotAbstract()
|
||||
.Should()
|
||||
.BeSealed()
|
||||
.GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccessful,
|
||||
$"All value objects should be sealed (immutable). Violations: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
|
||||
}
|
||||
}
|
||||
41
colaflow-api/tests/ColaFlow.Domain.Tests.csproj.template
Normal file
41
colaflow-api/tests/ColaFlow.Domain.Tests.csproj.template
Normal file
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- xUnit Test Framework -->
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Test Utilities -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
|
||||
<!-- Code Coverage -->
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Project References -->
|
||||
<ProjectReference Include="..\..\src\ColaFlow.Domain\ColaFlow.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
332
colaflow-api/tests/ColaFlow.Domain.Tests/Aggregates/EpicTests.cs
Normal file
332
colaflow-api/tests/ColaFlow.Domain.Tests/Aggregates/EpicTests.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Epic entity
|
||||
/// </summary>
|
||||
public class EpicTests
|
||||
{
|
||||
#region Create Tests
|
||||
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldCreateEpic()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Epic 1";
|
||||
var description = "Epic Description";
|
||||
var projectId = ProjectId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var epic = Epic.Create(name, description, projectId, createdBy);
|
||||
|
||||
// Assert
|
||||
epic.Should().NotBeNull();
|
||||
epic.Name.Should().Be(name);
|
||||
epic.Description.Should().Be(description);
|
||||
epic.ProjectId.Should().Be(projectId);
|
||||
epic.Status.Should().Be(WorkItemStatus.ToDo);
|
||||
epic.Priority.Should().Be(TaskPriority.Medium);
|
||||
epic.CreatedBy.Should().Be(createdBy);
|
||||
epic.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
epic.UpdatedAt.Should().BeNull();
|
||||
epic.Stories.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullDescription_ShouldCreateEpicWithEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Epic 1";
|
||||
string? description = null;
|
||||
var projectId = ProjectId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var epic = Epic.Create(name, description!, projectId, createdBy);
|
||||
|
||||
// Assert
|
||||
epic.Should().NotBeNull();
|
||||
epic.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyName_ShouldThrowDomainException(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => Epic.Create(invalidName, "Description", projectId, createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Epic name cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNameExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var name = new string('A', 201);
|
||||
var projectId = ProjectId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => Epic.Create(name, "Description", projectId, createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Epic name cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNameExactly200Characters_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var name = new string('A', 200);
|
||||
var projectId = ProjectId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var epic = Epic.Create(name, "Description", projectId, createdBy);
|
||||
|
||||
// Assert
|
||||
epic.Should().NotBeNull();
|
||||
epic.Name.Should().Be(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateStory Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateStory_WithValidData_ShouldCreateStory()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
var storyTitle = "Story 1";
|
||||
var storyDescription = "Story Description";
|
||||
var priority = TaskPriority.High;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var story = epic.CreateStory(storyTitle, storyDescription, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
story.Should().NotBeNull();
|
||||
story.Title.Should().Be(storyTitle);
|
||||
story.Description.Should().Be(storyDescription);
|
||||
story.EpicId.Should().Be(epic.Id);
|
||||
story.Priority.Should().Be(priority);
|
||||
story.CreatedBy.Should().Be(createdBy);
|
||||
epic.Stories.Should().ContainSingle();
|
||||
epic.Stories.Should().Contain(story);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateStory_MultipleStories_ShouldAddToCollection()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var story1 = epic.CreateStory("Story 1", "Desc 1", TaskPriority.Low, createdBy);
|
||||
var story2 = epic.CreateStory("Story 2", "Desc 2", TaskPriority.Medium, createdBy);
|
||||
var story3 = epic.CreateStory("Story 3", "Desc 3", TaskPriority.High, createdBy);
|
||||
|
||||
// Assert
|
||||
epic.Stories.Should().HaveCount(3);
|
||||
epic.Stories.Should().Contain(new[] { story1, story2, story3 });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateDetails Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithValidData_ShouldUpdateEpic()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Original Name", "Original Description", ProjectId.Create(), UserId.Create());
|
||||
var originalCreatedAt = epic.CreatedAt;
|
||||
var newName = "Updated Name";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
epic.UpdateDetails(newName, newDescription);
|
||||
|
||||
// Assert
|
||||
epic.Name.Should().Be(newName);
|
||||
epic.Description.Should().Be(newDescription);
|
||||
epic.CreatedAt.Should().Be(originalCreatedAt);
|
||||
epic.UpdatedAt.Should().NotBeNull();
|
||||
epic.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithNullDescription_ShouldSetEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Original Name", "Original Description", ProjectId.Create(), UserId.Create());
|
||||
|
||||
// Act
|
||||
epic.UpdateDetails("Updated Name", null!);
|
||||
|
||||
// Assert
|
||||
epic.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void UpdateDetails_WithEmptyName_ShouldThrowDomainException(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Original Name", "Original Description", ProjectId.Create(), UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => epic.UpdateDetails(invalidName, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Epic name cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithNameExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Original Name", "Original Description", ProjectId.Create(), UserId.Create());
|
||||
var name = new string('A', 201);
|
||||
|
||||
// Act
|
||||
Action act = () => epic.UpdateDetails(name, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Epic name cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_WithValidStatus_ShouldUpdateStatus()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
var newStatus = WorkItemStatus.InProgress;
|
||||
|
||||
// Act
|
||||
epic.UpdateStatus(newStatus);
|
||||
|
||||
// Assert
|
||||
epic.Status.Should().Be(newStatus);
|
||||
epic.UpdatedAt.Should().NotBeNull();
|
||||
epic.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_ToAllStatuses_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
epic.UpdateStatus(WorkItemStatus.InProgress);
|
||||
epic.Status.Should().Be(WorkItemStatus.InProgress);
|
||||
|
||||
epic.UpdateStatus(WorkItemStatus.InReview);
|
||||
epic.Status.Should().Be(WorkItemStatus.InReview);
|
||||
|
||||
epic.UpdateStatus(WorkItemStatus.Done);
|
||||
epic.Status.Should().Be(WorkItemStatus.Done);
|
||||
|
||||
epic.UpdateStatus(WorkItemStatus.Blocked);
|
||||
epic.Status.Should().Be(WorkItemStatus.Blocked);
|
||||
|
||||
epic.UpdateStatus(WorkItemStatus.ToDo);
|
||||
epic.Status.Should().Be(WorkItemStatus.ToDo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdatePriority Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdatePriority_WithValidPriority_ShouldUpdatePriority()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
var newPriority = TaskPriority.Urgent;
|
||||
|
||||
// Act
|
||||
epic.UpdatePriority(newPriority);
|
||||
|
||||
// Assert
|
||||
epic.Priority.Should().Be(newPriority);
|
||||
epic.UpdatedAt.Should().NotBeNull();
|
||||
epic.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePriority_ToAllPriorities_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
epic.UpdatePriority(TaskPriority.Low);
|
||||
epic.Priority.Should().Be(TaskPriority.Low);
|
||||
|
||||
epic.UpdatePriority(TaskPriority.Medium);
|
||||
epic.Priority.Should().Be(TaskPriority.Medium);
|
||||
|
||||
epic.UpdatePriority(TaskPriority.High);
|
||||
epic.Priority.Should().Be(TaskPriority.High);
|
||||
|
||||
epic.UpdatePriority(TaskPriority.Urgent);
|
||||
epic.Priority.Should().Be(TaskPriority.Urgent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entity Characteristics Tests
|
||||
|
||||
[Fact]
|
||||
public void Stories_Collection_ShouldBeReadOnly()
|
||||
{
|
||||
// Arrange
|
||||
var epic = Epic.Create("Epic 1", "Description", ProjectId.Create(), UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
epic.Stories.Should().BeAssignableTo<IReadOnlyCollection<Story>>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Epic_ShouldHaveUniqueId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var projectId = ProjectId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
var epic1 = Epic.Create("Epic 1", "Description", projectId, createdBy);
|
||||
var epic2 = Epic.Create("Epic 2", "Description", projectId, createdBy);
|
||||
|
||||
// Assert
|
||||
epic1.Id.Should().NotBe(epic2.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Project aggregate root
|
||||
/// </summary>
|
||||
public class ProjectTests
|
||||
{
|
||||
#region Create Tests
|
||||
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldCreateProject()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var description = "Test Description";
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, description, key, ownerId);
|
||||
|
||||
// Assert
|
||||
project.Should().NotBeNull();
|
||||
project.Name.Should().Be(name);
|
||||
project.Description.Should().Be(description);
|
||||
project.Key.Value.Should().Be(key);
|
||||
project.OwnerId.Should().Be(ownerId);
|
||||
project.Status.Should().Be(ProjectStatus.Active);
|
||||
project.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
project.UpdatedAt.Should().BeNull();
|
||||
project.Epics.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldRaiseProjectCreatedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var description = "Test Description";
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, description, key, ownerId);
|
||||
|
||||
// Assert
|
||||
project.DomainEvents.Should().ContainSingle();
|
||||
var domainEvent = project.DomainEvents.First();
|
||||
domainEvent.Should().BeOfType<ProjectCreatedEvent>();
|
||||
|
||||
var createdEvent = (ProjectCreatedEvent)domainEvent;
|
||||
createdEvent.ProjectId.Should().Be(project.Id);
|
||||
createdEvent.ProjectName.Should().Be(name);
|
||||
createdEvent.CreatedBy.Should().Be(ownerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullDescription_ShouldCreateProjectWithEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
string? description = null;
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, description!, key, ownerId);
|
||||
|
||||
// Assert
|
||||
project.Should().NotBeNull();
|
||||
project.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyName_ShouldThrowDomainException(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => Project.Create(invalidName, "Description", key, ownerId);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project name cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNameExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var name = new string('A', 201);
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => Project.Create(name, "Description", key, ownerId);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project name cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNameExactly200Characters_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var name = new string('A', 200);
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create();
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, "Description", key, ownerId);
|
||||
|
||||
// Assert
|
||||
project.Should().NotBeNull();
|
||||
project.Name.Should().Be(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateDetails Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithValidData_ShouldUpdateProject()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Original Name", "Original Description", "TEST", UserId.Create());
|
||||
var originalCreatedAt = project.CreatedAt;
|
||||
var newName = "Updated Name";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
project.UpdateDetails(newName, newDescription);
|
||||
|
||||
// Assert
|
||||
project.Name.Should().Be(newName);
|
||||
project.Description.Should().Be(newDescription);
|
||||
project.CreatedAt.Should().Be(originalCreatedAt); // CreatedAt should not change
|
||||
project.UpdatedAt.Should().NotBeNull();
|
||||
project.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WhenCalled_ShouldRaiseProjectUpdatedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Original Name", "Original Description", "TEST", UserId.Create());
|
||||
project.ClearDomainEvents(); // Clear creation event
|
||||
var newName = "Updated Name";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
project.UpdateDetails(newName, newDescription);
|
||||
|
||||
// Assert
|
||||
project.DomainEvents.Should().ContainSingle();
|
||||
var domainEvent = project.DomainEvents.First();
|
||||
domainEvent.Should().BeOfType<ProjectUpdatedEvent>();
|
||||
|
||||
var updatedEvent = (ProjectUpdatedEvent)domainEvent;
|
||||
updatedEvent.ProjectId.Should().Be(project.Id);
|
||||
updatedEvent.Name.Should().Be(newName);
|
||||
updatedEvent.Description.Should().Be(newDescription);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithNullDescription_ShouldSetEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Original Name", "Original Description", "TEST", UserId.Create());
|
||||
|
||||
// Act
|
||||
project.UpdateDetails("Updated Name", null!);
|
||||
|
||||
// Assert
|
||||
project.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void UpdateDetails_WithEmptyName_ShouldThrowDomainException(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Original Name", "Original Description", "TEST", UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => project.UpdateDetails(invalidName, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project name cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithNameExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Original Name", "Original Description", "TEST", UserId.Create());
|
||||
var name = new string('A', 201);
|
||||
|
||||
// Act
|
||||
Action act = () => project.UpdateDetails(name, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project name cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateEpic Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateEpic_WithValidData_ShouldCreateEpic()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.ClearDomainEvents();
|
||||
var epicName = "Epic 1";
|
||||
var epicDescription = "Epic Description";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var epic = project.CreateEpic(epicName, epicDescription, createdBy);
|
||||
|
||||
// Assert
|
||||
epic.Should().NotBeNull();
|
||||
epic.Name.Should().Be(epicName);
|
||||
epic.Description.Should().Be(epicDescription);
|
||||
epic.ProjectId.Should().Be(project.Id);
|
||||
epic.CreatedBy.Should().Be(createdBy);
|
||||
project.Epics.Should().ContainSingle();
|
||||
project.Epics.Should().Contain(epic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEpic_WhenCalled_ShouldRaiseEpicCreatedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.ClearDomainEvents();
|
||||
var epicName = "Epic 1";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var epic = project.CreateEpic(epicName, "Epic Description", createdBy);
|
||||
|
||||
// Assert
|
||||
project.DomainEvents.Should().ContainSingle();
|
||||
var domainEvent = project.DomainEvents.First();
|
||||
domainEvent.Should().BeOfType<EpicCreatedEvent>();
|
||||
|
||||
var epicCreatedEvent = (EpicCreatedEvent)domainEvent;
|
||||
epicCreatedEvent.EpicId.Should().Be(epic.Id);
|
||||
epicCreatedEvent.EpicName.Should().Be(epicName);
|
||||
epicCreatedEvent.ProjectId.Should().Be(project.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEpic_InArchivedProject_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.Archive();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => project.CreateEpic("Epic 1", "Description", createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Cannot create epic in an archived project");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEpic_MultipleEpics_ShouldAddToCollection()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var epic1 = project.CreateEpic("Epic 1", "Description 1", createdBy);
|
||||
var epic2 = project.CreateEpic("Epic 2", "Description 2", createdBy);
|
||||
var epic3 = project.CreateEpic("Epic 3", "Description 3", createdBy);
|
||||
|
||||
// Assert
|
||||
project.Epics.Should().HaveCount(3);
|
||||
project.Epics.Should().Contain(new[] { epic1, epic2, epic3 });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive Tests
|
||||
|
||||
[Fact]
|
||||
public void Archive_ActiveProject_ShouldArchiveProject()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
|
||||
// Act
|
||||
project.Archive();
|
||||
|
||||
// Assert
|
||||
project.Status.Should().Be(ProjectStatus.Archived);
|
||||
project.UpdatedAt.Should().NotBeNull();
|
||||
project.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_WhenCalled_ShouldRaiseProjectArchivedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
project.Archive();
|
||||
|
||||
// Assert
|
||||
project.DomainEvents.Should().ContainSingle();
|
||||
var domainEvent = project.DomainEvents.First();
|
||||
domainEvent.Should().BeOfType<ProjectArchivedEvent>();
|
||||
|
||||
var archivedEvent = (ProjectArchivedEvent)domainEvent;
|
||||
archivedEvent.ProjectId.Should().Be(project.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_AlreadyArchivedProject_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.Archive();
|
||||
|
||||
// Act
|
||||
Action act = () => project.Archive();
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project is already archived");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Activate Tests
|
||||
|
||||
[Fact]
|
||||
public void Activate_ArchivedProject_ShouldActivateProject()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.Archive();
|
||||
|
||||
// Act
|
||||
project.Activate();
|
||||
|
||||
// Assert
|
||||
project.Status.Should().Be(ProjectStatus.Active);
|
||||
project.UpdatedAt.Should().NotBeNull();
|
||||
project.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_AlreadyActiveProject_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => project.Activate();
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project is already active");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_ArchivedProjectWithEpics_ShouldActivateSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
project.CreateEpic("Epic 1", "Description", UserId.Create());
|
||||
project.Archive();
|
||||
|
||||
// Act
|
||||
project.Activate();
|
||||
|
||||
// Assert
|
||||
project.Status.Should().Be(ProjectStatus.Active);
|
||||
project.Epics.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Aggregate Boundary Tests
|
||||
|
||||
[Fact]
|
||||
public void Epics_Collection_ShouldBeReadOnly()
|
||||
{
|
||||
// Arrange
|
||||
var project = Project.Create("Test Project", "Description", "TEST", UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
project.Epics.Should().BeAssignableTo<IReadOnlyCollection<Epic>>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_ShouldHaveUniqueId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var project1 = Project.Create("Project 1", "Description", "PRJ1", UserId.Create());
|
||||
var project2 = Project.Create("Project 2", "Description", "PRJ2", UserId.Create());
|
||||
|
||||
// Assert
|
||||
project1.Id.Should().NotBe(project2.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Story entity
|
||||
/// </summary>
|
||||
public class StoryTests
|
||||
{
|
||||
#region Create Tests
|
||||
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldCreateStory()
|
||||
{
|
||||
// Arrange
|
||||
var title = "User Story 1";
|
||||
var description = "Story Description";
|
||||
var epicId = EpicId.Create();
|
||||
var priority = TaskPriority.High;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var story = Story.Create(title, description, epicId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
story.Should().NotBeNull();
|
||||
story.Title.Should().Be(title);
|
||||
story.Description.Should().Be(description);
|
||||
story.EpicId.Should().Be(epicId);
|
||||
story.Status.Should().Be(WorkItemStatus.ToDo);
|
||||
story.Priority.Should().Be(priority);
|
||||
story.CreatedBy.Should().Be(createdBy);
|
||||
story.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
story.UpdatedAt.Should().BeNull();
|
||||
story.EstimatedHours.Should().BeNull();
|
||||
story.ActualHours.Should().BeNull();
|
||||
story.AssigneeId.Should().BeNull();
|
||||
story.Tasks.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullDescription_ShouldCreateStoryWithEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var title = "User Story 1";
|
||||
string? description = null;
|
||||
var epicId = EpicId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var story = Story.Create(title, description!, epicId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
story.Should().NotBeNull();
|
||||
story.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyTitle_ShouldThrowDomainException(string invalidTitle)
|
||||
{
|
||||
// Arrange
|
||||
var epicId = EpicId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => Story.Create(invalidTitle, "Description", epicId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Story title cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithTitleExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var title = new string('A', 201);
|
||||
var epicId = EpicId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => Story.Create(title, "Description", epicId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Story title cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithTitleExactly200Characters_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var title = new string('A', 200);
|
||||
var epicId = EpicId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var story = Story.Create(title, "Description", epicId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
story.Should().NotBeNull();
|
||||
story.Title.Should().Be(title);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateTask Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateTask_WithValidData_ShouldCreateTask()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var taskTitle = "Task 1";
|
||||
var taskDescription = "Task Description";
|
||||
var priority = TaskPriority.Urgent;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var task = story.CreateTask(taskTitle, taskDescription, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
task.Should().NotBeNull();
|
||||
task.Title.Should().Be(taskTitle);
|
||||
task.Description.Should().Be(taskDescription);
|
||||
task.StoryId.Should().Be(story.Id);
|
||||
task.Priority.Should().Be(priority);
|
||||
task.CreatedBy.Should().Be(createdBy);
|
||||
story.Tasks.Should().ContainSingle();
|
||||
story.Tasks.Should().Contain(task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTask_MultipleTasks_ShouldAddToCollection()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var task1 = story.CreateTask("Task 1", "Desc 1", TaskPriority.Low, createdBy);
|
||||
var task2 = story.CreateTask("Task 2", "Desc 2", TaskPriority.Medium, createdBy);
|
||||
var task3 = story.CreateTask("Task 3", "Desc 3", TaskPriority.High, createdBy);
|
||||
|
||||
// Assert
|
||||
story.Tasks.Should().HaveCount(3);
|
||||
story.Tasks.Should().Contain(new[] { task1, task2, task3 });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateDetails Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithValidData_ShouldUpdateStory()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Original Title", "Original Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var originalCreatedAt = story.CreatedAt;
|
||||
var newTitle = "Updated Title";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
story.UpdateDetails(newTitle, newDescription);
|
||||
|
||||
// Assert
|
||||
story.Title.Should().Be(newTitle);
|
||||
story.Description.Should().Be(newDescription);
|
||||
story.CreatedAt.Should().Be(originalCreatedAt);
|
||||
story.UpdatedAt.Should().NotBeNull();
|
||||
story.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithNullDescription_ShouldSetEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Original Title", "Original Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
story.UpdateDetails("Updated Title", null!);
|
||||
|
||||
// Assert
|
||||
story.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void UpdateDetails_WithEmptyTitle_ShouldThrowDomainException(string invalidTitle)
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Original Title", "Original Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => story.UpdateDetails(invalidTitle, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Story title cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithTitleExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Original Title", "Original Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var title = new string('A', 201);
|
||||
|
||||
// Act
|
||||
Action act = () => story.UpdateDetails(title, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Story title cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_WithValidStatus_ShouldUpdateStatus()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var newStatus = WorkItemStatus.InProgress;
|
||||
|
||||
// Act
|
||||
story.UpdateStatus(newStatus);
|
||||
|
||||
// Assert
|
||||
story.Status.Should().Be(newStatus);
|
||||
story.UpdatedAt.Should().NotBeNull();
|
||||
story.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_ToAllStatuses_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
story.UpdateStatus(WorkItemStatus.InProgress);
|
||||
story.Status.Should().Be(WorkItemStatus.InProgress);
|
||||
|
||||
story.UpdateStatus(WorkItemStatus.InReview);
|
||||
story.Status.Should().Be(WorkItemStatus.InReview);
|
||||
|
||||
story.UpdateStatus(WorkItemStatus.Done);
|
||||
story.Status.Should().Be(WorkItemStatus.Done);
|
||||
|
||||
story.UpdateStatus(WorkItemStatus.Blocked);
|
||||
story.Status.Should().Be(WorkItemStatus.Blocked);
|
||||
|
||||
story.UpdateStatus(WorkItemStatus.ToDo);
|
||||
story.Status.Should().Be(WorkItemStatus.ToDo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AssignTo Tests
|
||||
|
||||
[Fact]
|
||||
public void AssignTo_WithValidUserId_ShouldAssignStory()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var assigneeId = UserId.Create();
|
||||
|
||||
// Act
|
||||
story.AssignTo(assigneeId);
|
||||
|
||||
// Assert
|
||||
story.AssigneeId.Should().Be(assigneeId);
|
||||
story.UpdatedAt.Should().NotBeNull();
|
||||
story.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignTo_ReassignToDifferentUser_ShouldUpdateAssignee()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var firstAssignee = UserId.Create();
|
||||
var secondAssignee = UserId.Create();
|
||||
|
||||
// Act
|
||||
story.AssignTo(firstAssignee);
|
||||
story.AssignTo(secondAssignee);
|
||||
|
||||
// Assert
|
||||
story.AssigneeId.Should().Be(secondAssignee);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateEstimate Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_WithValidHours_ShouldUpdateEstimate()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var hours = 8.5m;
|
||||
|
||||
// Act
|
||||
story.UpdateEstimate(hours);
|
||||
|
||||
// Assert
|
||||
story.EstimatedHours.Should().Be(hours);
|
||||
story.UpdatedAt.Should().NotBeNull();
|
||||
story.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_WithZeroHours_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
story.UpdateEstimate(0);
|
||||
|
||||
// Assert
|
||||
story.EstimatedHours.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_WithNegativeHours_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => story.UpdateEstimate(-1);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Estimated hours cannot be negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_MultipleUpdates_ShouldOverwritePreviousValue()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
story.UpdateEstimate(8);
|
||||
story.UpdateEstimate(16);
|
||||
|
||||
// Assert
|
||||
story.EstimatedHours.Should().Be(16);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LogActualHours Tests
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_WithValidHours_ShouldLogHours()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var hours = 10.5m;
|
||||
|
||||
// Act
|
||||
story.LogActualHours(hours);
|
||||
|
||||
// Assert
|
||||
story.ActualHours.Should().Be(hours);
|
||||
story.UpdatedAt.Should().NotBeNull();
|
||||
story.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_WithZeroHours_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
story.LogActualHours(0);
|
||||
|
||||
// Assert
|
||||
story.ActualHours.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_WithNegativeHours_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => story.LogActualHours(-1);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Actual hours cannot be negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_MultipleUpdates_ShouldOverwritePreviousValue()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
story.LogActualHours(8);
|
||||
story.LogActualHours(12);
|
||||
|
||||
// Assert
|
||||
story.ActualHours.Should().Be(12);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entity Characteristics Tests
|
||||
|
||||
[Fact]
|
||||
public void Tasks_Collection_ShouldBeReadOnly()
|
||||
{
|
||||
// Arrange
|
||||
var story = Story.Create("Story 1", "Description", EpicId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
story.Tasks.Should().BeAssignableTo<IReadOnlyCollection<WorkTask>>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Story_ShouldHaveUniqueId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var epicId = EpicId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
var story1 = Story.Create("Story 1", "Description", epicId, TaskPriority.Medium, createdBy);
|
||||
var story2 = Story.Create("Story 2", "Description", epicId, TaskPriority.Medium, createdBy);
|
||||
|
||||
// Assert
|
||||
story1.Id.Should().NotBe(story2.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for WorkTask entity
|
||||
/// </summary>
|
||||
public class WorkTaskTests
|
||||
{
|
||||
#region Create Tests
|
||||
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldCreateTask()
|
||||
{
|
||||
// Arrange
|
||||
var title = "Task 1";
|
||||
var description = "Task Description";
|
||||
var storyId = StoryId.Create();
|
||||
var priority = TaskPriority.High;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var task = WorkTask.Create(title, description, storyId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
task.Should().NotBeNull();
|
||||
task.Title.Should().Be(title);
|
||||
task.Description.Should().Be(description);
|
||||
task.StoryId.Should().Be(storyId);
|
||||
task.Status.Should().Be(WorkItemStatus.ToDo);
|
||||
task.Priority.Should().Be(priority);
|
||||
task.CreatedBy.Should().Be(createdBy);
|
||||
task.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
task.UpdatedAt.Should().BeNull();
|
||||
task.EstimatedHours.Should().BeNull();
|
||||
task.ActualHours.Should().BeNull();
|
||||
task.AssigneeId.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullDescription_ShouldCreateTaskWithEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var title = "Task 1";
|
||||
string? description = null;
|
||||
var storyId = StoryId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var task = WorkTask.Create(title, description!, storyId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
task.Should().NotBeNull();
|
||||
task.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyTitle_ShouldThrowDomainException(string invalidTitle)
|
||||
{
|
||||
// Arrange
|
||||
var storyId = StoryId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => WorkTask.Create(invalidTitle, "Description", storyId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Task title cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithTitleExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var title = new string('A', 201);
|
||||
var storyId = StoryId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
Action act = () => WorkTask.Create(title, "Description", storyId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Task title cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithTitleExactly200Characters_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var title = new string('A', 200);
|
||||
var storyId = StoryId.Create();
|
||||
var priority = TaskPriority.Medium;
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var task = WorkTask.Create(title, "Description", storyId, priority, createdBy);
|
||||
|
||||
// Assert
|
||||
task.Should().NotBeNull();
|
||||
task.Title.Should().Be(title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithDifferentPriorities_ShouldSetCorrectPriority()
|
||||
{
|
||||
// Arrange
|
||||
var storyId = StoryId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var taskLow = WorkTask.Create("Task Low", "Desc", storyId, TaskPriority.Low, createdBy);
|
||||
var taskMedium = WorkTask.Create("Task Medium", "Desc", storyId, TaskPriority.Medium, createdBy);
|
||||
var taskHigh = WorkTask.Create("Task High", "Desc", storyId, TaskPriority.High, createdBy);
|
||||
var taskUrgent = WorkTask.Create("Task Urgent", "Desc", storyId, TaskPriority.Urgent, createdBy);
|
||||
|
||||
// Assert
|
||||
taskLow.Priority.Should().Be(TaskPriority.Low);
|
||||
taskMedium.Priority.Should().Be(TaskPriority.Medium);
|
||||
taskHigh.Priority.Should().Be(TaskPriority.High);
|
||||
taskUrgent.Priority.Should().Be(TaskPriority.Urgent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateDetails Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithValidData_ShouldUpdateTask()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Original Title", "Original Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var originalCreatedAt = task.CreatedAt;
|
||||
var newTitle = "Updated Title";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
task.UpdateDetails(newTitle, newDescription);
|
||||
|
||||
// Assert
|
||||
task.Title.Should().Be(newTitle);
|
||||
task.Description.Should().Be(newDescription);
|
||||
task.CreatedAt.Should().Be(originalCreatedAt);
|
||||
task.UpdatedAt.Should().NotBeNull();
|
||||
task.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithNullDescription_ShouldSetEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Original Title", "Original Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
task.UpdateDetails("Updated Title", null!);
|
||||
|
||||
// Assert
|
||||
task.Description.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void UpdateDetails_WithEmptyTitle_ShouldThrowDomainException(string invalidTitle)
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Original Title", "Original Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => task.UpdateDetails(invalidTitle, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Task title cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_WithTitleExceeding200Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Original Title", "Original Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var title = new string('A', 201);
|
||||
|
||||
// Act
|
||||
Action act = () => task.UpdateDetails(title, "Updated Description");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Task title cannot exceed 200 characters");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_WithValidStatus_ShouldUpdateStatus()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var newStatus = WorkItemStatus.InProgress;
|
||||
|
||||
// Act
|
||||
task.UpdateStatus(newStatus);
|
||||
|
||||
// Assert
|
||||
task.Status.Should().Be(newStatus);
|
||||
task.UpdatedAt.Should().NotBeNull();
|
||||
task.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_ToAllStatuses_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
task.UpdateStatus(WorkItemStatus.InProgress);
|
||||
task.Status.Should().Be(WorkItemStatus.InProgress);
|
||||
|
||||
task.UpdateStatus(WorkItemStatus.InReview);
|
||||
task.Status.Should().Be(WorkItemStatus.InReview);
|
||||
|
||||
task.UpdateStatus(WorkItemStatus.Done);
|
||||
task.Status.Should().Be(WorkItemStatus.Done);
|
||||
|
||||
task.UpdateStatus(WorkItemStatus.Blocked);
|
||||
task.Status.Should().Be(WorkItemStatus.Blocked);
|
||||
|
||||
task.UpdateStatus(WorkItemStatus.ToDo);
|
||||
task.Status.Should().Be(WorkItemStatus.ToDo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AssignTo Tests
|
||||
|
||||
[Fact]
|
||||
public void AssignTo_WithValidUserId_ShouldAssignTask()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var assigneeId = UserId.Create();
|
||||
|
||||
// Act
|
||||
task.AssignTo(assigneeId);
|
||||
|
||||
// Assert
|
||||
task.AssigneeId.Should().Be(assigneeId);
|
||||
task.UpdatedAt.Should().NotBeNull();
|
||||
task.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignTo_ReassignToDifferentUser_ShouldUpdateAssignee()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var firstAssignee = UserId.Create();
|
||||
var secondAssignee = UserId.Create();
|
||||
|
||||
// Act
|
||||
task.AssignTo(firstAssignee);
|
||||
task.AssignTo(secondAssignee);
|
||||
|
||||
// Assert
|
||||
task.AssigneeId.Should().Be(secondAssignee);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdatePriority Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdatePriority_WithValidPriority_ShouldUpdatePriority()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Low, UserId.Create());
|
||||
var newPriority = TaskPriority.Urgent;
|
||||
|
||||
// Act
|
||||
task.UpdatePriority(newPriority);
|
||||
|
||||
// Assert
|
||||
task.Priority.Should().Be(newPriority);
|
||||
task.UpdatedAt.Should().NotBeNull();
|
||||
task.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePriority_ToAllPriorities_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act & Assert
|
||||
task.UpdatePriority(TaskPriority.Low);
|
||||
task.Priority.Should().Be(TaskPriority.Low);
|
||||
|
||||
task.UpdatePriority(TaskPriority.Medium);
|
||||
task.Priority.Should().Be(TaskPriority.Medium);
|
||||
|
||||
task.UpdatePriority(TaskPriority.High);
|
||||
task.Priority.Should().Be(TaskPriority.High);
|
||||
|
||||
task.UpdatePriority(TaskPriority.Urgent);
|
||||
task.Priority.Should().Be(TaskPriority.Urgent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateEstimate Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_WithValidHours_ShouldUpdateEstimate()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var hours = 4.5m;
|
||||
|
||||
// Act
|
||||
task.UpdateEstimate(hours);
|
||||
|
||||
// Assert
|
||||
task.EstimatedHours.Should().Be(hours);
|
||||
task.UpdatedAt.Should().NotBeNull();
|
||||
task.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_WithZeroHours_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
task.UpdateEstimate(0);
|
||||
|
||||
// Assert
|
||||
task.EstimatedHours.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_WithNegativeHours_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => task.UpdateEstimate(-1);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Estimated hours cannot be negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEstimate_MultipleUpdates_ShouldOverwritePreviousValue()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
task.UpdateEstimate(4);
|
||||
task.UpdateEstimate(8);
|
||||
|
||||
// Assert
|
||||
task.EstimatedHours.Should().Be(8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LogActualHours Tests
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_WithValidHours_ShouldLogHours()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
var hours = 5.5m;
|
||||
|
||||
// Act
|
||||
task.LogActualHours(hours);
|
||||
|
||||
// Assert
|
||||
task.ActualHours.Should().Be(hours);
|
||||
task.UpdatedAt.Should().NotBeNull();
|
||||
task.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_WithZeroHours_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
task.LogActualHours(0);
|
||||
|
||||
// Assert
|
||||
task.ActualHours.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_WithNegativeHours_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
Action act = () => task.LogActualHours(-1);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Actual hours cannot be negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogActualHours_MultipleUpdates_ShouldOverwritePreviousValue()
|
||||
{
|
||||
// Arrange
|
||||
var task = WorkTask.Create("Task 1", "Description", StoryId.Create(), TaskPriority.Medium, UserId.Create());
|
||||
|
||||
// Act
|
||||
task.LogActualHours(4);
|
||||
task.LogActualHours(6);
|
||||
|
||||
// Assert
|
||||
task.ActualHours.Should().Be(6);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entity Characteristics Tests
|
||||
|
||||
[Fact]
|
||||
public void WorkTask_ShouldHaveUniqueId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var storyId = StoryId.Create();
|
||||
var createdBy = UserId.Create();
|
||||
var task1 = WorkTask.Create("Task 1", "Description", storyId, TaskPriority.Medium, createdBy);
|
||||
var task2 = WorkTask.Create("Task 2", "Description", storyId, TaskPriority.Medium, createdBy);
|
||||
|
||||
// Assert
|
||||
task1.Id.Should().NotBe(task2.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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="coverlet.msbuild" 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\ProjectManagement\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,226 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Domain Events
|
||||
/// </summary>
|
||||
public class DomainEventsTests
|
||||
{
|
||||
#region ProjectCreatedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectCreatedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var projectName = "Test Project";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
|
||||
// Assert
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.ProjectName.Should().Be(projectName);
|
||||
@event.CreatedBy.Should().Be(createdBy);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectCreatedEvent_ShouldBeRecord()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var projectName = "Test Project";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var event1 = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
var event2 = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
|
||||
// Assert - Records with same values should be equal
|
||||
event1.ProjectId.Should().Be(event2.ProjectId);
|
||||
event1.ProjectName.Should().Be(event2.ProjectName);
|
||||
event1.CreatedBy.Should().Be(event2.CreatedBy);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProjectUpdatedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectUpdatedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var name = "Updated Project";
|
||||
var description = "Updated Description";
|
||||
|
||||
// Act
|
||||
var @event = new ProjectUpdatedEvent(projectId, name, description);
|
||||
|
||||
// Assert
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.Name.Should().Be(name);
|
||||
@event.Description.Should().Be(description);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectUpdatedEvent_WithNullDescription_ShouldAcceptNull()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var name = "Updated Project";
|
||||
|
||||
// Act
|
||||
var @event = new ProjectUpdatedEvent(projectId, name, null!);
|
||||
|
||||
// Assert
|
||||
@event.Description.Should().BeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProjectArchivedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectArchivedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new ProjectArchivedEvent(projectId);
|
||||
|
||||
// Assert
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectArchivedEvent_ShouldBeRecord()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var event1 = new ProjectArchivedEvent(projectId);
|
||||
var event2 = new ProjectArchivedEvent(projectId);
|
||||
|
||||
// Assert - Records with same values should be equal
|
||||
event1.ProjectId.Should().Be(event2.ProjectId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EpicCreatedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void EpicCreatedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var epicId = EpicId.Create();
|
||||
var epicName = "Epic 1";
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new EpicCreatedEvent(epicId, epicName, projectId);
|
||||
|
||||
// Assert
|
||||
@event.EpicId.Should().Be(epicId);
|
||||
@event.EpicName.Should().Be(epicName);
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpicCreatedEvent_ShouldBeRecord()
|
||||
{
|
||||
// Arrange
|
||||
var epicId = EpicId.Create();
|
||||
var epicName = "Epic 1";
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var event1 = new EpicCreatedEvent(epicId, epicName, projectId);
|
||||
var event2 = new EpicCreatedEvent(epicId, epicName, projectId);
|
||||
|
||||
// Assert - Records with same values should be equal
|
||||
event1.EpicId.Should().Be(event2.EpicId);
|
||||
event1.EpicName.Should().Be(event2.EpicName);
|
||||
event1.ProjectId.Should().Be(event2.ProjectId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Timing Tests
|
||||
|
||||
[Fact]
|
||||
public void DomainEvents_OccurredOn_ShouldBeUtcTime()
|
||||
{
|
||||
// Arrange & Act
|
||||
var projectCreatedEvent = new ProjectCreatedEvent(ProjectId.Create(), "Test", UserId.Create());
|
||||
var projectUpdatedEvent = new ProjectUpdatedEvent(ProjectId.Create(), "Test", "Desc");
|
||||
var projectArchivedEvent = new ProjectArchivedEvent(ProjectId.Create());
|
||||
var epicCreatedEvent = new EpicCreatedEvent(EpicId.Create(), "Epic", ProjectId.Create());
|
||||
|
||||
// Assert
|
||||
projectCreatedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
projectUpdatedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
projectArchivedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
epicCreatedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DomainEvents_OccurredOn_ShouldBeSetAutomatically()
|
||||
{
|
||||
// Arrange
|
||||
var beforeCreation = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
var @event = new ProjectCreatedEvent(ProjectId.Create(), "Test", UserId.Create());
|
||||
|
||||
// Assert
|
||||
var afterCreation = DateTime.UtcNow;
|
||||
@event.OccurredOn.Should().BeOnOrAfter(beforeCreation);
|
||||
@event.OccurredOn.Should().BeOnOrBefore(afterCreation);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Immutability Tests
|
||||
|
||||
[Fact]
|
||||
public void DomainEvents_ShouldBeImmutable()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var projectName = "Test Project";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
var originalProjectId = @event.ProjectId;
|
||||
var originalProjectName = @event.ProjectName;
|
||||
var originalCreatedBy = @event.CreatedBy;
|
||||
var originalOccurredOn = @event.OccurredOn;
|
||||
|
||||
// Try to access properties multiple times
|
||||
var projectId1 = @event.ProjectId;
|
||||
var projectName1 = @event.ProjectName;
|
||||
var createdBy1 = @event.CreatedBy;
|
||||
var occurredOn1 = @event.OccurredOn;
|
||||
|
||||
// Assert - Properties should not change
|
||||
projectId1.Should().Be(originalProjectId);
|
||||
projectName1.Should().Be(originalProjectName);
|
||||
createdBy1.Should().Be(originalCreatedBy);
|
||||
occurredOn1.Should().Be(originalOccurredOn);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Enumeration-based value objects
|
||||
/// </summary>
|
||||
public class EnumerationTests
|
||||
{
|
||||
#region ProjectStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Active_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
ProjectStatus.Active.Id.Should().Be(1);
|
||||
ProjectStatus.Active.Name.Should().Be("Active");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Archived_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
ProjectStatus.Archived.Id.Should().Be(2);
|
||||
ProjectStatus.Archived.Name.Should().Be("Archived");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_OnHold_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
ProjectStatus.OnHold.Id.Should().Be(3);
|
||||
ProjectStatus.OnHold.Name.Should().Be("On Hold");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Equals_WithSameStatus_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = ProjectStatus.Active;
|
||||
var status2 = ProjectStatus.Active;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().Be(status2);
|
||||
status1.Equals(status2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Equals_WithDifferentStatus_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = ProjectStatus.Active;
|
||||
var status2 = ProjectStatus.Archived;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().NotBe(status2);
|
||||
status1.Equals(status2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_ToString_ShouldReturnName()
|
||||
{
|
||||
// Arrange
|
||||
var status = ProjectStatus.Active;
|
||||
|
||||
// Act
|
||||
var result = status.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Active");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WorkItemStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_ToDo_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.ToDo.Id.Should().Be(1);
|
||||
WorkItemStatus.ToDo.Name.Should().Be("To Do");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_InProgress_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.InProgress.Id.Should().Be(2);
|
||||
WorkItemStatus.InProgress.Name.Should().Be("In Progress");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_InReview_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.InReview.Id.Should().Be(3);
|
||||
WorkItemStatus.InReview.Name.Should().Be("In Review");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Done_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.Done.Id.Should().Be(4);
|
||||
WorkItemStatus.Done.Name.Should().Be("Done");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Blocked_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.Blocked.Id.Should().Be(5);
|
||||
WorkItemStatus.Blocked.Name.Should().Be("Blocked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Equals_WithSameStatus_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = WorkItemStatus.ToDo;
|
||||
var status2 = WorkItemStatus.ToDo;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().Be(status2);
|
||||
status1.Equals(status2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Equals_WithDifferentStatus_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = WorkItemStatus.ToDo;
|
||||
var status2 = WorkItemStatus.Done;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().NotBe(status2);
|
||||
status1.Equals(status2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_ToString_ShouldReturnName()
|
||||
{
|
||||
// Arrange
|
||||
var status = WorkItemStatus.InProgress;
|
||||
|
||||
// Act
|
||||
var result = status.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be("In Progress");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TaskPriority Tests
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Low_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.Low.Id.Should().Be(1);
|
||||
TaskPriority.Low.Name.Should().Be("Low");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Medium_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.Medium.Id.Should().Be(2);
|
||||
TaskPriority.Medium.Name.Should().Be("Medium");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_High_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.High.Id.Should().Be(3);
|
||||
TaskPriority.High.Name.Should().Be("High");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Urgent_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.Urgent.Id.Should().Be(4);
|
||||
TaskPriority.Urgent.Name.Should().Be("Urgent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Equals_WithSamePriority_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var priority1 = TaskPriority.High;
|
||||
var priority2 = TaskPriority.High;
|
||||
|
||||
// Act & Assert
|
||||
priority1.Should().Be(priority2);
|
||||
priority1.Equals(priority2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Equals_WithDifferentPriority_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var priority1 = TaskPriority.Low;
|
||||
var priority2 = TaskPriority.Urgent;
|
||||
|
||||
// Act & Assert
|
||||
priority1.Should().NotBe(priority2);
|
||||
priority1.Equals(priority2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_ToString_ShouldReturnName()
|
||||
{
|
||||
// Arrange
|
||||
var priority = TaskPriority.Medium;
|
||||
|
||||
// Act
|
||||
var result = priority.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Medium");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_ShouldBeOrderedByImportance()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
TaskPriority.Low.Id.Should().BeLessThan(TaskPriority.Medium.Id);
|
||||
TaskPriority.Medium.Id.Should().BeLessThan(TaskPriority.High.Id);
|
||||
TaskPriority.High.Id.Should().BeLessThan(TaskPriority.Urgent.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ProjectId value object
|
||||
/// </summary>
|
||||
public class ProjectIdTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Assert
|
||||
projectId.Should().NotBeNull();
|
||||
projectId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithGuid_ShouldCreateProjectIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var projectId = ProjectId.Create(guid);
|
||||
|
||||
// Assert
|
||||
projectId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void From_WithGuid_ShouldCreateProjectId()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var projectId = ProjectId.From(guid);
|
||||
|
||||
// Assert
|
||||
projectId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_MultipleCalls_ShouldGenerateDifferentGuids()
|
||||
{
|
||||
// Act
|
||||
var projectId1 = ProjectId.Create();
|
||||
var projectId2 = ProjectId.Create();
|
||||
|
||||
// Assert
|
||||
projectId1.Value.Should().NotBe(projectId2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId1 = ProjectId.Create(guid);
|
||||
var projectId2 = ProjectId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
projectId1.Should().Be(projectId2);
|
||||
projectId1.Equals(projectId2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithDifferentValue_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var projectId1 = ProjectId.Create();
|
||||
var projectId2 = ProjectId.Create();
|
||||
|
||||
// Act & Assert
|
||||
projectId1.Should().NotBe(projectId2);
|
||||
projectId1.Equals(projectId2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithSameValue_ShouldReturnSameHashCode()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId1 = ProjectId.Create(guid);
|
||||
var projectId2 = ProjectId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
projectId1.GetHashCode().Should().Be(projectId2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_ShouldReturnGuidString()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId = ProjectId.Create(guid);
|
||||
|
||||
// Act
|
||||
var result = projectId.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be(guid.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueObject_ShouldBeImmutable()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId = ProjectId.Create(guid);
|
||||
var originalValue = projectId.Value;
|
||||
|
||||
// Act - Try to get the value multiple times
|
||||
var value1 = projectId.Value;
|
||||
var value2 = projectId.Value;
|
||||
|
||||
// Assert
|
||||
value1.Should().Be(originalValue);
|
||||
value2.Should().Be(originalValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ProjectKey value object
|
||||
/// </summary>
|
||||
public class ProjectKeyTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithValidKey_ShouldCreateProjectKey()
|
||||
{
|
||||
// Arrange
|
||||
var key = "COLA";
|
||||
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Should().NotBeNull();
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("A")]
|
||||
[InlineData("AB")]
|
||||
[InlineData("ABC")]
|
||||
[InlineData("ABCD")]
|
||||
[InlineData("ABCDEFGHIJ")] // 10 characters - max allowed
|
||||
public void Create_WithValidLengthKeys_ShouldSucceed(string key)
|
||||
{
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("TEST")]
|
||||
[InlineData("COLA")]
|
||||
[InlineData("FLOW")]
|
||||
[InlineData("PROJECT1")]
|
||||
[InlineData("ABC123")]
|
||||
public void Create_WithUppercaseLettersAndNumbers_ShouldSucceed(string key)
|
||||
{
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyKey_ShouldThrowDomainException(string invalidKey)
|
||||
{
|
||||
// Act
|
||||
Action act = () => ProjectKey.Create(invalidKey);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project key cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithKeyExceeding10Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var key = "ABCDEFGHIJK"; // 11 characters
|
||||
|
||||
// Act
|
||||
Action act = () => ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project key cannot exceed 10 characters");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("test")] // lowercase
|
||||
[InlineData("Test")] // mixed case
|
||||
[InlineData("TEST-1")] // hyphen
|
||||
[InlineData("TEST_1")] // underscore
|
||||
[InlineData("TEST 1")] // space
|
||||
[InlineData("TEST.1")] // dot
|
||||
[InlineData("TEST@1")] // special char
|
||||
public void Create_WithInvalidCharacters_ShouldThrowDomainException(string invalidKey)
|
||||
{
|
||||
// Act
|
||||
Action act = () => ProjectKey.Create(invalidKey);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project key must contain only uppercase letters and numbers");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var projectKey1 = ProjectKey.Create(key);
|
||||
var projectKey2 = ProjectKey.Create(key);
|
||||
|
||||
// Act & Assert
|
||||
projectKey1.Should().Be(projectKey2);
|
||||
projectKey1.Equals(projectKey2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithDifferentValue_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var projectKey1 = ProjectKey.Create("TEST1");
|
||||
var projectKey2 = ProjectKey.Create("TEST2");
|
||||
|
||||
// Act & Assert
|
||||
projectKey1.Should().NotBe(projectKey2);
|
||||
projectKey1.Equals(projectKey2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithSameValue_ShouldReturnSameHashCode()
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var projectKey1 = ProjectKey.Create(key);
|
||||
var projectKey2 = ProjectKey.Create(key);
|
||||
|
||||
// Act & Assert
|
||||
projectKey1.GetHashCode().Should().Be(projectKey2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_ShouldReturnKeyValue()
|
||||
{
|
||||
// Arrange
|
||||
var key = "COLA";
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Act
|
||||
var result = projectKey.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be(key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueObject_ShouldBeImmutable()
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
var originalValue = projectKey.Value;
|
||||
|
||||
// Act - Try to get the value multiple times
|
||||
var value1 = projectKey.Value;
|
||||
var value2 = projectKey.Value;
|
||||
|
||||
// Assert
|
||||
value1.Should().Be(originalValue);
|
||||
value2.Should().Be(originalValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("123")]
|
||||
[InlineData("789")]
|
||||
[InlineData("1234567890")]
|
||||
public void Create_WithOnlyNumbers_ShouldSucceed(string key)
|
||||
{
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for strongly-typed ID value objects (EpicId, StoryId, TaskId, UserId)
|
||||
/// </summary>
|
||||
public class StronglyTypedIdTests
|
||||
{
|
||||
#region EpicId Tests
|
||||
|
||||
[Fact]
|
||||
public void EpicId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var epicId = EpicId.Create();
|
||||
|
||||
// Assert
|
||||
epicId.Should().NotBeNull();
|
||||
epicId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpicId_Create_WithGuid_ShouldCreateEpicIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var epicId = EpicId.Create(guid);
|
||||
|
||||
// Assert
|
||||
epicId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpicId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var epicId1 = EpicId.Create(guid);
|
||||
var epicId2 = EpicId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
epicId1.Should().Be(epicId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StoryId Tests
|
||||
|
||||
[Fact]
|
||||
public void StoryId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var storyId = StoryId.Create();
|
||||
|
||||
// Assert
|
||||
storyId.Should().NotBeNull();
|
||||
storyId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoryId_Create_WithGuid_ShouldCreateStoryIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var storyId = StoryId.Create(guid);
|
||||
|
||||
// Assert
|
||||
storyId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoryId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var storyId1 = StoryId.Create(guid);
|
||||
var storyId2 = StoryId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
storyId1.Should().Be(storyId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TaskId Tests
|
||||
|
||||
[Fact]
|
||||
public void TaskId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var taskId = TaskId.Create();
|
||||
|
||||
// Assert
|
||||
taskId.Should().NotBeNull();
|
||||
taskId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskId_Create_WithGuid_ShouldCreateTaskIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var taskId = TaskId.Create(guid);
|
||||
|
||||
// Assert
|
||||
taskId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var taskId1 = TaskId.Create(guid);
|
||||
var taskId2 = TaskId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
taskId1.Should().Be(taskId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UserId Tests
|
||||
|
||||
[Fact]
|
||||
public void UserId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var userId = UserId.Create();
|
||||
|
||||
// Assert
|
||||
userId.Should().NotBeNull();
|
||||
userId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserId_Create_WithGuid_ShouldCreateUserIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var userId = UserId.Create(guid);
|
||||
|
||||
// Assert
|
||||
userId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var userId1 = UserId.Create(guid);
|
||||
var userId2 = UserId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
userId1.Should().Be(userId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Safety Tests
|
||||
|
||||
[Fact]
|
||||
public void DifferentIdTypes_WithSameGuid_ShouldNotBeEqual()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId = ProjectId.Create(guid);
|
||||
var epicId = EpicId.Create(guid);
|
||||
|
||||
// Act & Assert - They are different types, so should not be equal
|
||||
projectId.Should().NotBe((object)epicId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleCalls_ShouldGenerateDifferentGuids()
|
||||
{
|
||||
// Act
|
||||
var id1 = ProjectId.Create();
|
||||
var id2 = ProjectId.Create();
|
||||
var id3 = EpicId.Create();
|
||||
var id4 = StoryId.Create();
|
||||
var id5 = TaskId.Create();
|
||||
|
||||
// Assert
|
||||
id1.Value.Should().NotBe(id2.Value);
|
||||
id1.Value.Should().NotBe(id3.Value);
|
||||
id1.Value.Should().NotBe(id4.Value);
|
||||
id1.Value.Should().NotBe(id5.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
57
colaflow-api/tests/ColaFlow.IntegrationTests.csproj.template
Normal file
57
colaflow-api/tests/ColaFlow.IntegrationTests.csproj.template
Normal file
@@ -0,0 +1,57 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- xUnit Test Framework -->
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Test Utilities -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
|
||||
<!-- Integration Testing -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
|
||||
|
||||
<!-- Testcontainers -->
|
||||
<PackageReference Include="Testcontainers" Version="3.9.0" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="3.9.0" />
|
||||
<PackageReference Include="Testcontainers.Redis" Version="3.9.0" />
|
||||
|
||||
<!-- Database -->
|
||||
<PackageReference Include="Npgsql" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||
|
||||
<!-- Code Coverage -->
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Project References -->
|
||||
<ProjectReference Include="..\..\src\ColaFlow.API\ColaFlow.API.csproj" />
|
||||
<ProjectReference Include="..\..\src\ColaFlow.Infrastructure\ColaFlow.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
<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="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\ColaFlow.API\ColaFlow.API.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
10
colaflow-api/tests/ColaFlow.IntegrationTests/UnitTest1.cs
Normal file
10
colaflow-api/tests/ColaFlow.IntegrationTests/UnitTest1.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.IntegrationTests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
135
colaflow-api/tests/ExampleDomainTest.cs
Normal file
135
colaflow-api/tests/ExampleDomainTest.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Example Domain Unit Test for Project Aggregate
|
||||
/// Based on M1-Architecture-Design.md Section 8.2
|
||||
/// </summary>
|
||||
public class ProjectTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ValidData_ShouldCreateProject()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var description = "Test Description";
|
||||
var key = "TEST";
|
||||
var ownerId = Guid.NewGuid(); // UserId.Create(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
// var project = Project.Create(name, description, key, ownerId);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after Project aggregate is implemented
|
||||
// project.Should().NotBeNull();
|
||||
// project.Name.Should().Be(name);
|
||||
// project.Key.Value.Should().Be(key);
|
||||
// project.Status.Should().Be(ProjectStatus.Active);
|
||||
// project.DomainEvents.Should().ContainSingle(e => e is ProjectCreatedEvent);
|
||||
|
||||
// Placeholder assertion for template
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_EmptyName_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var name = "";
|
||||
var key = "TEST";
|
||||
var ownerId = Guid.NewGuid(); // UserId.Create(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
// Action act = () => Project.Create(name, "", key, ownerId);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after Project aggregate is implemented
|
||||
// act.Should().Throw<DomainException>()
|
||||
// .WithMessage("Project name cannot be empty");
|
||||
|
||||
// Placeholder assertion for template
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_KeyTooLong_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var key = "VERYLONGKEY"; // > 10 characters
|
||||
var ownerId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
// Action act = () => Project.Create(name, "", key, ownerId);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after Project aggregate is implemented
|
||||
// act.Should().Throw<DomainException>()
|
||||
// .WithMessage("Project key cannot exceed 10 characters");
|
||||
|
||||
// Placeholder assertion for template
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_ValidData_ShouldUpdateProject()
|
||||
{
|
||||
// Arrange
|
||||
// var project = Project.Create("Original", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
var newName = "Updated Project";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
// project.UpdateDetails(newName, newDescription);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after Project aggregate is implemented
|
||||
// project.Name.Should().Be(newName);
|
||||
// project.Description.Should().Be(newDescription);
|
||||
// project.UpdatedAt.Should().NotBeNull();
|
||||
// project.DomainEvents.Should().Contain(e => e is ProjectUpdatedEvent);
|
||||
|
||||
// Placeholder assertion for template
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_ActiveProject_ShouldArchiveSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
// var project = Project.Create("Test", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
|
||||
// Act
|
||||
// project.Archive();
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after Project aggregate is implemented
|
||||
// project.Status.Should().Be(ProjectStatus.Archived);
|
||||
// project.UpdatedAt.Should().NotBeNull();
|
||||
// project.DomainEvents.Should().Contain(e => e is ProjectArchivedEvent);
|
||||
|
||||
// Placeholder assertion for template
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_AlreadyArchived_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
// var project = Project.Create("Test", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
// project.Archive();
|
||||
|
||||
// Act
|
||||
// Action act = () => project.Archive();
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after Project aggregate is implemented
|
||||
// act.Should().Throw<DomainException>()
|
||||
// .WithMessage("Project is already archived");
|
||||
|
||||
// Placeholder assertion for template
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
}
|
||||
241
colaflow-api/tests/ExampleIntegrationTest.cs
Normal file
241
colaflow-api/tests/ExampleIntegrationTest.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.IntegrationTests.API;
|
||||
|
||||
/// <summary>
|
||||
/// Example API Integration Test for Projects Controller
|
||||
/// Based on M1-Architecture-Design.md Section 8.3
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To use this test, you need to:
|
||||
/// 1. Implement ColaFlowWebApplicationFactory with your Program and DbContext types
|
||||
/// 2. Implement actual API endpoints
|
||||
/// 3. Implement DTOs
|
||||
/// </remarks>
|
||||
public class ProjectsApiTests // : IClassFixture<ColaFlowWebApplicationFactory<Program, ColaFlowDbContext>>
|
||||
{
|
||||
// private readonly HttpClient _client;
|
||||
// private readonly ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> _factory;
|
||||
|
||||
// public ProjectsApiTests(ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> factory)
|
||||
// {
|
||||
// _factory = factory;
|
||||
// _client = factory.CreateClient();
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task GetProjects_ReturnsSuccessStatusCode()
|
||||
{
|
||||
// Arrange
|
||||
// No setup needed
|
||||
|
||||
// Act
|
||||
// var response = await _client.GetAsync("/api/v1/projects");
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateProject_ValidData_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
// var createRequest = new CreateProjectDto
|
||||
// {
|
||||
// Name = "Test Project",
|
||||
// Description = "Test Description",
|
||||
// Key = "TEST"
|
||||
// };
|
||||
|
||||
// Act
|
||||
// var response = await _client.PostAsJsonAsync("/api/v1/projects", createRequest);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
// var project = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
// project.Should().NotBeNull();
|
||||
// project!.Name.Should().Be("Test Project");
|
||||
// project.Key.Should().Be("TEST");
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateProject_DuplicateKey_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
// using var scope = _factory.CreateScope();
|
||||
// var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
// Seed existing project with key "TEST"
|
||||
// var existingProject = Project.Create("Existing", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
// await dbContext.Projects.AddAsync(existingProject);
|
||||
// await dbContext.SaveChangesAsync();
|
||||
|
||||
// var createRequest = new CreateProjectDto
|
||||
// {
|
||||
// Name = "New Project",
|
||||
// Description = "Description",
|
||||
// Key = "TEST" // Duplicate key
|
||||
// };
|
||||
|
||||
// Act
|
||||
// var response = await _client.PostAsJsonAsync("/api/v1/projects", createRequest);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetProject_ExistingId_ReturnsProject()
|
||||
{
|
||||
// Arrange
|
||||
// using var scope = _factory.CreateScope();
|
||||
// var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
// Seed test project
|
||||
// var project = Project.Create("Test Project", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
// await dbContext.Projects.AddAsync(project);
|
||||
// await dbContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
// var response = await _client.GetAsync($"/api/v1/projects/{project.Id.Value}");
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
// var returnedProject = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
// returnedProject.Should().NotBeNull();
|
||||
// returnedProject!.Name.Should().Be("Test Project");
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetProject_NonExistingId_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
// var nonExistingId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
// var response = await _client.GetAsync($"/api/v1/projects/{nonExistingId}");
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateProject_ValidData_ReturnsUpdated()
|
||||
{
|
||||
// Arrange
|
||||
// using var scope = _factory.CreateScope();
|
||||
// var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
// Seed test project
|
||||
// var project = Project.Create("Original", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
// await dbContext.Projects.AddAsync(project);
|
||||
// await dbContext.SaveChangesAsync();
|
||||
|
||||
// var updateRequest = new UpdateProjectDto
|
||||
// {
|
||||
// Name = "Updated Project",
|
||||
// Description = "Updated Description"
|
||||
// };
|
||||
|
||||
// Act
|
||||
// var response = await _client.PutAsJsonAsync($"/api/v1/projects/{project.Id.Value}", updateRequest);
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
// var updatedProject = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
// updatedProject.Should().NotBeNull();
|
||||
// updatedProject!.Name.Should().Be("Updated Project");
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteProject_ExistingId_ReturnsNoContent()
|
||||
{
|
||||
// Arrange
|
||||
// using var scope = _factory.CreateScope();
|
||||
// var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
// Seed test project
|
||||
// var project = Project.Create("Test", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
// await dbContext.Projects.AddAsync(project);
|
||||
// await dbContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
// var response = await _client.DeleteAsync($"/api/v1/projects/{project.Id.Value}");
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
||||
|
||||
// Verify soft delete
|
||||
// var deletedProject = await dbContext.Projects.FindAsync(project.Id);
|
||||
// deletedProject.Should().BeNull(); // Filtered by global query filter
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetKanbanBoard_ExistingProject_ReturnsBoard()
|
||||
{
|
||||
// Arrange
|
||||
// using var scope = _factory.CreateScope();
|
||||
// var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
// Seed test project with tasks
|
||||
// var project = Project.Create("Test", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
// var epic = project.CreateEpic("Epic 1", "Description", UserId.Create(Guid.NewGuid()));
|
||||
// var story = epic.CreateStory("Story 1", "Description", TaskPriority.High, UserId.Create(Guid.NewGuid()));
|
||||
// story.CreateTask("Task 1", "Description", TaskPriority.Medium, UserId.Create(Guid.NewGuid()));
|
||||
|
||||
// await dbContext.Projects.AddAsync(project);
|
||||
// await dbContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
// var response = await _client.GetAsync($"/api/v1/projects/{project.Id.Value}/kanban");
|
||||
|
||||
// Assert
|
||||
// TODO: Uncomment after API is implemented
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
// var kanban = await response.Content.ReadFromJsonAsync<KanbanBoardDto>();
|
||||
// kanban.Should().NotBeNull();
|
||||
// kanban!.Columns.Should().HaveCount(4); // To Do, In Progress, Review, Done
|
||||
|
||||
// Placeholder assertion for template
|
||||
await Task.CompletedTask;
|
||||
true.Should().BeTrue("This is a placeholder test");
|
||||
}
|
||||
}
|
||||
197
colaflow-api/tests/IntegrationTestBase.cs
Normal file
197
colaflow-api/tests/IntegrationTestBase.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Configurations;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Testcontainers.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for integration tests that require PostgreSQL and Redis
|
||||
/// Uses Testcontainers to spin up isolated database instances
|
||||
/// </summary>
|
||||
public abstract class IntegrationTestBase : IAsyncLifetime
|
||||
{
|
||||
// PostgreSQL Container
|
||||
protected PostgreSqlContainer PostgresContainer { get; private set; } = null!;
|
||||
|
||||
// Redis Container
|
||||
protected RedisContainer RedisContainer { get; private set; } = null!;
|
||||
|
||||
// Connection Strings
|
||||
protected string PostgresConnectionString => PostgresContainer.GetConnectionString();
|
||||
protected string RedisConnectionString => RedisContainer.GetConnectionString();
|
||||
|
||||
/// <summary>
|
||||
/// Initialize containers before tests
|
||||
/// Called by xUnit before any test in the class runs
|
||||
/// </summary>
|
||||
public virtual async Task InitializeAsync()
|
||||
{
|
||||
// Create PostgreSQL container
|
||||
PostgresContainer = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
.WithDatabase("colaflow_test")
|
||||
.WithUsername("colaflow_test")
|
||||
.WithPassword("colaflow_test_password")
|
||||
.WithCleanUp(true)
|
||||
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432))
|
||||
.Build();
|
||||
|
||||
// Create Redis container
|
||||
RedisContainer = new RedisBuilder()
|
||||
.WithImage("redis:7-alpine")
|
||||
.WithCleanUp(true)
|
||||
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
|
||||
.Build();
|
||||
|
||||
// Start containers in parallel
|
||||
await Task.WhenAll(
|
||||
PostgresContainer.StartAsync(),
|
||||
RedisContainer.StartAsync()
|
||||
);
|
||||
|
||||
// Optional: Run migrations or seed data
|
||||
await SeedDatabaseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup containers after tests
|
||||
/// Called by xUnit after all tests in the class complete
|
||||
/// </summary>
|
||||
public virtual async Task DisposeAsync()
|
||||
{
|
||||
// Stop containers in parallel
|
||||
await Task.WhenAll(
|
||||
PostgresContainer.StopAsync(),
|
||||
RedisContainer.StopAsync()
|
||||
);
|
||||
|
||||
// Dispose containers
|
||||
await PostgresContainer.DisposeAsync();
|
||||
await RedisContainer.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed database with test data
|
||||
/// Override in derived classes for custom seeding
|
||||
/// </summary>
|
||||
protected virtual async Task SeedDatabaseAsync()
|
||||
{
|
||||
// Example: Create tables, seed data, etc.
|
||||
await using var connection = new NpgsqlConnection(PostgresConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Create extensions
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
CREATE EXTENSION IF NOT EXISTS ""uuid-ossp"";
|
||||
CREATE EXTENSION IF NOT EXISTS ""pg_trgm"";
|
||||
";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create DbContextOptions for Entity Framework Core
|
||||
/// </summary>
|
||||
protected DbContextOptions<TContext> CreateDbContextOptions<TContext>()
|
||||
where TContext : DbContext
|
||||
{
|
||||
return new DbContextOptionsBuilder<TContext>()
|
||||
.UseNpgsql(PostgresConnectionString)
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors()
|
||||
.Options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute SQL command on test database
|
||||
/// </summary>
|
||||
protected async Task ExecuteSqlAsync(string sql)
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(PostgresConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean database tables for test isolation
|
||||
/// </summary>
|
||||
protected async Task CleanDatabaseAsync()
|
||||
{
|
||||
await ExecuteSqlAsync(@"
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
-- Disable triggers
|
||||
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
|
||||
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
|
||||
END LOOP;
|
||||
END $$;
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection fixture for sharing Testcontainers across multiple test classes
|
||||
/// Use [Collection("IntegrationTests")] attribute on test classes
|
||||
/// </summary>
|
||||
[CollectionDefinition("IntegrationTests")]
|
||||
public class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture>
|
||||
{
|
||||
// This class has no code, and is never created.
|
||||
// Its purpose is simply to be the place to apply [CollectionDefinition]
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared fixture for integration tests
|
||||
/// Containers are created once and shared across test classes
|
||||
/// </summary>
|
||||
public class IntegrationTestFixture : IAsyncLifetime
|
||||
{
|
||||
public PostgreSqlContainer PostgresContainer { get; private set; } = null!;
|
||||
public RedisContainer RedisContainer { get; private set; } = null!;
|
||||
|
||||
public string PostgresConnectionString => PostgresContainer.GetConnectionString();
|
||||
public string RedisConnectionString => RedisContainer.GetConnectionString();
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Create containers
|
||||
PostgresContainer = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
.WithDatabase("colaflow_test")
|
||||
.WithUsername("colaflow_test")
|
||||
.WithPassword("colaflow_test_password")
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
RedisContainer = new RedisBuilder()
|
||||
.WithImage("redis:7-alpine")
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
// Start containers
|
||||
await Task.WhenAll(
|
||||
PostgresContainer.StartAsync(),
|
||||
RedisContainer.StartAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await Task.WhenAll(
|
||||
PostgresContainer.DisposeAsync().AsTask(),
|
||||
RedisContainer.DisposeAsync().AsTask()
|
||||
);
|
||||
}
|
||||
}
|
||||
631
colaflow-api/tests/README.md
Normal file
631
colaflow-api/tests/README.md
Normal file
@@ -0,0 +1,631 @@
|
||||
# ColaFlow Testing Guide
|
||||
|
||||
This document explains the testing strategy, setup, and best practices for ColaFlow project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Testing Philosophy](#testing-philosophy)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [CI/CD Integration](#cicd-integration)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Testing Philosophy
|
||||
|
||||
ColaFlow follows the **Test Pyramid** approach:
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E Tests (5%)
|
||||
/ \ - Critical user flows
|
||||
/------\
|
||||
/ \ Integration Tests (15%)
|
||||
/ \ - API endpoints
|
||||
/ \ - Database operations
|
||||
/--------------\
|
||||
/ \ Unit Tests (80%)
|
||||
------------------ - Domain logic
|
||||
- Application services
|
||||
- Business rules
|
||||
```
|
||||
|
||||
### Quality Standards
|
||||
|
||||
- **Minimum Code Coverage**: 80%
|
||||
- **Target Code Coverage**: 90%+
|
||||
- **Critical Path Coverage**: 100%
|
||||
- **All tests must**:
|
||||
- Be repeatable and deterministic
|
||||
- Run independently (no order dependency)
|
||||
- Clean up after themselves
|
||||
- Have clear assertions and error messages
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── ColaFlow.Domain.Tests/ # Domain unit tests
|
||||
│ ├── Aggregates/
|
||||
│ │ ├── ProjectTests.cs
|
||||
│ │ ├── EpicTests.cs
|
||||
│ │ └── TaskTests.cs
|
||||
│ ├── ValueObjects/
|
||||
│ │ ├── ProjectIdTests.cs
|
||||
│ │ └── TaskPriorityTests.cs
|
||||
│ └── DomainEvents/
|
||||
│ └── EventHandlerTests.cs
|
||||
│
|
||||
├── ColaFlow.Application.Tests/ # Application layer tests
|
||||
│ ├── Commands/
|
||||
│ │ ├── CreateProjectCommandTests.cs
|
||||
│ │ └── UpdateProjectCommandTests.cs
|
||||
│ ├── Queries/
|
||||
│ │ ├── GetProjectByIdQueryTests.cs
|
||||
│ │ └── GetKanbanBoardQueryTests.cs
|
||||
│ └── Behaviors/
|
||||
│ ├── ValidationBehaviorTests.cs
|
||||
│ └── TransactionBehaviorTests.cs
|
||||
│
|
||||
├── ColaFlow.IntegrationTests/ # Integration tests
|
||||
│ ├── API/
|
||||
│ │ ├── ProjectsApiTests.cs
|
||||
│ │ ├── TasksApiTests.cs
|
||||
│ │ └── WorkflowsApiTests.cs
|
||||
│ ├── Infrastructure/
|
||||
│ │ ├── IntegrationTestBase.cs
|
||||
│ │ └── WebApplicationFactoryBase.cs
|
||||
│ └── Database/
|
||||
│ ├── RepositoryTests.cs
|
||||
│ └── MigrationTests.cs
|
||||
│
|
||||
├── ExampleDomainTest.cs # Template domain test
|
||||
├── ExampleIntegrationTest.cs # Template integration test
|
||||
├── IntegrationTestBase.cs # Base class for integration tests
|
||||
├── WebApplicationFactoryBase.cs # WebApplicationFactory setup
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **.NET 9 SDK** (includes testing tools)
|
||||
- **Docker Desktop** (for Testcontainers)
|
||||
- **IDE**: Visual Studio 2022, JetBrains Rider, or VS Code
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Ensure Docker Desktop is running**:
|
||||
```bash
|
||||
docker --version
|
||||
docker ps
|
||||
```
|
||||
|
||||
2. **Restore NuGet packages** (if not already done):
|
||||
```bash
|
||||
cd tests
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
3. **Verify test projects build**:
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### Creating Test Projects
|
||||
|
||||
If test projects don't exist yet, use the provided templates:
|
||||
|
||||
```bash
|
||||
# Domain Tests
|
||||
cd tests
|
||||
dotnet new xunit -n ColaFlow.Domain.Tests
|
||||
cp ColaFlow.Domain.Tests.csproj.template ColaFlow.Domain.Tests/ColaFlow.Domain.Tests.csproj
|
||||
|
||||
# Application Tests
|
||||
dotnet new xunit -n ColaFlow.Application.Tests
|
||||
cp ColaFlow.Application.Tests.csproj.template ColaFlow.Application.Tests/ColaFlow.Application.Tests.csproj
|
||||
|
||||
# Integration Tests
|
||||
dotnet new xunit -n ColaFlow.IntegrationTests
|
||||
cp ColaFlow.IntegrationTests.csproj.template ColaFlow.IntegrationTests/ColaFlow.IntegrationTests.csproj
|
||||
|
||||
# Restore packages
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All Tests
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
dotnet test
|
||||
|
||||
# From tests directory
|
||||
cd tests
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### Run Specific Test Project
|
||||
|
||||
```bash
|
||||
# Domain tests only
|
||||
dotnet test ColaFlow.Domain.Tests/ColaFlow.Domain.Tests.csproj
|
||||
|
||||
# Integration tests only
|
||||
dotnet test ColaFlow.IntegrationTests/ColaFlow.IntegrationTests.csproj
|
||||
```
|
||||
|
||||
### Run Specific Test Class
|
||||
|
||||
```bash
|
||||
dotnet test --filter FullyQualifiedName~ProjectTests
|
||||
```
|
||||
|
||||
### Run Specific Test Method
|
||||
|
||||
```bash
|
||||
dotnet test --filter FullyQualifiedName~ProjectTests.Create_ValidData_ShouldCreateProject
|
||||
```
|
||||
|
||||
### Run Tests by Category
|
||||
|
||||
```bash
|
||||
# Run only unit tests
|
||||
dotnet test --filter Category=Unit
|
||||
|
||||
# Run only integration tests
|
||||
dotnet test --filter Category=Integration
|
||||
|
||||
# Exclude slow tests
|
||||
dotnet test --filter Category!=Slow
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```bash
|
||||
# Run tests in parallel (default)
|
||||
dotnet test --parallel
|
||||
|
||||
# Run tests sequentially (for debugging)
|
||||
dotnet test --parallel none
|
||||
```
|
||||
|
||||
### Verbose Output
|
||||
|
||||
```bash
|
||||
# Detailed output
|
||||
dotnet test --logger "console;verbosity=detailed"
|
||||
|
||||
# Minimal output
|
||||
dotnet test --logger "console;verbosity=minimal"
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Unit Tests (Domain Layer)
|
||||
|
||||
**Example**: Testing Project Aggregate
|
||||
|
||||
```csharp
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
public class ProjectTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ValidData_ShouldCreateProject()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var description = "Test Description";
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, description, key, ownerId);
|
||||
|
||||
// Assert
|
||||
project.Should().NotBeNull();
|
||||
project.Name.Should().Be(name);
|
||||
project.Key.Value.Should().Be(key);
|
||||
project.Status.Should().Be(ProjectStatus.Active);
|
||||
project.DomainEvents.Should().ContainSingle(e => e is ProjectCreatedEvent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
[InlineData(" ")]
|
||||
public void Create_InvalidName_ShouldThrowException(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
Action act = () => Project.Create(invalidName, "", key, ownerId);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project name cannot be empty");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Application Layer Tests (CQRS)
|
||||
|
||||
**Example**: Testing Command Handler
|
||||
|
||||
```csharp
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Application.Tests.Commands;
|
||||
|
||||
public class CreateProjectCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||
private readonly Mock<ICurrentUserService> _currentUserServiceMock;
|
||||
private readonly CreateProjectCommandHandler _handler;
|
||||
|
||||
public CreateProjectCommandHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||
_currentUserServiceMock = new Mock<ICurrentUserService>();
|
||||
_handler = new CreateProjectCommandHandler(
|
||||
_projectRepositoryMock.Object,
|
||||
_unitOfWorkMock.Object,
|
||||
_currentUserServiceMock.Object,
|
||||
Mock.Of<IMapper>(),
|
||||
Mock.Of<ILogger<CreateProjectCommandHandler>>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidCommand_CreatesProject()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateProjectCommand
|
||||
{
|
||||
Name = "Test Project",
|
||||
Description = "Description",
|
||||
Key = "TEST"
|
||||
};
|
||||
|
||||
_currentUserServiceMock.Setup(x => x.UserId).Returns(Guid.NewGuid());
|
||||
_projectRepositoryMock.Setup(x => x.GetByKeyAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync((Project?)null);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, default);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_projectRepositoryMock.Verify(x => x.AddAsync(It.IsAny<Project>(), default), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.CommitAsync(default), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DuplicateKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateProjectCommand { Name = "Test", Key = "TEST" };
|
||||
var existingProject = Project.Create("Existing", "", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
|
||||
_projectRepositoryMock.Setup(x => x.GetByKeyAsync("TEST", default))
|
||||
.ReturnsAsync(existingProject);
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _handler.Handle(command, default);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<DomainException>()
|
||||
.WithMessage("*already exists*");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests (API)
|
||||
|
||||
**Example**: Testing API Endpoint
|
||||
|
||||
```csharp
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.IntegrationTests.API;
|
||||
|
||||
[Collection("IntegrationTests")]
|
||||
public class ProjectsApiTests : IClassFixture<ColaFlowWebApplicationFactory<Program, ColaFlowDbContext>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> _factory;
|
||||
|
||||
public ProjectsApiTests(ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateProject_ValidData_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var createRequest = new CreateProjectDto
|
||||
{
|
||||
Name = "Test Project",
|
||||
Description = "Test Description",
|
||||
Key = "TEST"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/projects", createRequest);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var project = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
project.Should().NotBeNull();
|
||||
project!.Name.Should().Be("Test Project");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetProject_ExistingId_ReturnsProject()
|
||||
{
|
||||
// Arrange - Seed data
|
||||
using var scope = _factory.CreateScope();
|
||||
var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
var project = Project.Create("Test", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
await dbContext.Projects.AddAsync(project);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/api/v1/projects/{project.Id.Value}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var returnedProject = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
returnedProject.Should().NotBeNull();
|
||||
returnedProject!.Name.Should().Be("Test");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Generate Coverage Report
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
|
||||
# Generate HTML report (requires ReportGenerator)
|
||||
dotnet tool install -g dotnet-reportgenerator-globaltool
|
||||
reportgenerator -reports:coverage.opencover.xml -targetdir:coveragereport -reporttypes:Html
|
||||
|
||||
# Open report
|
||||
start coveragereport/index.html # Windows
|
||||
open coveragereport/index.html # Mac
|
||||
```
|
||||
|
||||
### Coverage Thresholds
|
||||
|
||||
Configure in test project `.csproj`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<CoverletOutputFormat>opencover</CoverletOutputFormat>
|
||||
<Threshold>80</Threshold>
|
||||
<ThresholdType>line,branch,method</ThresholdType>
|
||||
<ThresholdStat>total</ThresholdStat>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Exclude from Coverage
|
||||
|
||||
```csharp
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class Startup { }
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Tests run automatically on every push and pull request. See `.github/workflows/test.yml`.
|
||||
|
||||
### Local CI Simulation
|
||||
|
||||
```bash
|
||||
# Simulate CI environment
|
||||
dotnet clean
|
||||
dotnet restore
|
||||
dotnet build --no-restore
|
||||
dotnet test --no-build --verbosity normal
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### General Principles
|
||||
|
||||
1. **Arrange-Act-Assert (AAA) Pattern**
|
||||
```csharp
|
||||
[Fact]
|
||||
public void TestMethod()
|
||||
{
|
||||
// Arrange - Setup test data and dependencies
|
||||
var input = "test";
|
||||
|
||||
// Act - Execute the method under test
|
||||
var result = MethodUnderTest(input);
|
||||
|
||||
// Assert - Verify the result
|
||||
result.Should().Be("expected");
|
||||
}
|
||||
```
|
||||
|
||||
2. **One Assertion Per Test** (when practical)
|
||||
- Makes failures easier to diagnose
|
||||
- Exception: Related assertions (e.g., checking object properties)
|
||||
|
||||
3. **Test Naming Convention**
|
||||
```
|
||||
MethodName_StateUnderTest_ExpectedBehavior
|
||||
```
|
||||
Examples:
|
||||
- `Create_ValidData_ShouldCreateProject`
|
||||
- `Create_EmptyName_ShouldThrowException`
|
||||
- `GetById_NonExistentId_ReturnsNotFound`
|
||||
|
||||
4. **Test Independence**
|
||||
- Tests should not depend on execution order
|
||||
- Each test should clean up after itself
|
||||
- Use test fixtures for shared setup
|
||||
|
||||
5. **Avoid Test Logic**
|
||||
- No loops, conditionals, or complex logic in tests
|
||||
- Tests should be simple and readable
|
||||
|
||||
### Domain Tests
|
||||
|
||||
- Test business rules and invariants
|
||||
- Test domain events are raised
|
||||
- Test value object equality
|
||||
- No mocking (pure unit tests)
|
||||
|
||||
### Application Tests
|
||||
|
||||
- Mock infrastructure dependencies (repositories, services)
|
||||
- Test command/query handlers
|
||||
- Test validation logic
|
||||
- Test MediatR pipeline behaviors
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Use Testcontainers for real databases
|
||||
- Test complete request/response flows
|
||||
- Test database operations
|
||||
- Test authentication/authorization
|
||||
- Clean database between tests
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Keep unit tests fast (< 100ms each)
|
||||
- Integration tests can be slower (< 5s each)
|
||||
- Use `[Fact(Skip = "Reason")]` for slow tests during development
|
||||
- Run slow tests in CI only
|
||||
|
||||
### Data Builders
|
||||
|
||||
Use builder pattern for complex test data:
|
||||
|
||||
```csharp
|
||||
public class ProjectBuilder
|
||||
{
|
||||
private string _name = "Test Project";
|
||||
private string _key = "TEST";
|
||||
|
||||
public ProjectBuilder WithName(string name)
|
||||
{
|
||||
_name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProjectBuilder WithKey(string key)
|
||||
{
|
||||
_key = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Project Build()
|
||||
{
|
||||
return Project.Create(_name, "Description", _key, UserId.Create(Guid.NewGuid()));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
var project = new ProjectBuilder()
|
||||
.WithName("Custom Project")
|
||||
.WithKey("CUSTOM")
|
||||
.Build();
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker Not Running
|
||||
|
||||
**Error**: `Unable to connect to Docker daemon`
|
||||
|
||||
**Solution**: Start Docker Desktop and ensure it's fully initialized.
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
**Error**: `Address already in use`
|
||||
|
||||
**Solution**: Stop conflicting services or use different ports in `docker-compose.yml`.
|
||||
|
||||
### Test Database Not Clean
|
||||
|
||||
**Issue**: Tests fail due to leftover data
|
||||
|
||||
**Solution**: Use `CleanDatabaseAsync()` in test setup or use Testcontainers (auto-cleanup).
|
||||
|
||||
### Slow Tests
|
||||
|
||||
**Issue**: Integration tests taking too long
|
||||
|
||||
**Solutions**:
|
||||
- Use Testcontainers' shared fixture (reuse containers)
|
||||
- Optimize database queries
|
||||
- Use in-memory database for simple tests
|
||||
- Run integration tests selectively
|
||||
|
||||
### Flaky Tests
|
||||
|
||||
**Issue**: Tests pass/fail intermittently
|
||||
|
||||
**Common causes**:
|
||||
- Race conditions (async/await issues)
|
||||
- Time-dependent assertions
|
||||
- External service dependencies
|
||||
- Database transaction issues
|
||||
|
||||
**Solutions**:
|
||||
- Use proper async/await
|
||||
- Mock time-dependent code
|
||||
- Use Testcontainers for isolation
|
||||
- Ensure proper transaction handling
|
||||
|
||||
## Resources
|
||||
|
||||
- [xUnit Documentation](https://xunit.net/)
|
||||
- [FluentAssertions Documentation](https://fluentassertions.com/)
|
||||
- [Testcontainers Documentation](https://dotnet.testcontainers.org/)
|
||||
- [Architecture Design](../docs/M1-Architecture-Design.md)
|
||||
- [Docker Setup](../DOCKER-README.md)
|
||||
|
||||
## Support
|
||||
|
||||
For testing issues:
|
||||
1. Check this README
|
||||
2. Review test examples in this directory
|
||||
3. Consult architecture documentation
|
||||
4. Ask team for help
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-02
|
||||
**Maintained By**: QA Team
|
||||
**Quality Standard**: 80%+ Coverage, All Tests Green
|
||||
295
colaflow-api/tests/SPRINT1-TEST-REPORT-TEMPLATE.md
Normal file
295
colaflow-api/tests/SPRINT1-TEST-REPORT-TEMPLATE.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Sprint 1 Test Report
|
||||
|
||||
**Sprint**: Sprint 1
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**QA Engineer**: [Your Name]
|
||||
**Status**: [In Progress / Completed / Blocked]
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### Overall Status
|
||||
|
||||
| Metric | Target | Actual | Status |
|
||||
|--------|--------|--------|--------|
|
||||
| **Unit Test Coverage** | >= 80% | [X]% | [✅/❌] |
|
||||
| **Integration Test Coverage** | >= 15% | [X]% | [✅/❌] |
|
||||
| **Tests Passing** | 100% | [X]% | [✅/❌] |
|
||||
| **Critical Bugs** | 0 | [X] | [✅/❌] |
|
||||
| **High Priority Bugs** | < 3 | [X] | [✅/❌] |
|
||||
| **Docker Environment** | Working | [Working/Issues] | [✅/❌] |
|
||||
|
||||
### Summary
|
||||
|
||||
[Brief 2-3 sentence summary of sprint test status]
|
||||
|
||||
---
|
||||
|
||||
## Test Execution Results
|
||||
|
||||
### Unit Tests
|
||||
|
||||
#### Domain Tests (`ColaFlow.Domain.Tests`)
|
||||
|
||||
| Test Suite | Total Tests | Passed | Failed | Skipped | Coverage |
|
||||
|------------|-------------|--------|--------|---------|----------|
|
||||
| Project Aggregate | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Epic Entity | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Story Entity | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Task Entity | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Value Objects | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Domain Events | [X] | [X] | [X] | [X] | [X]% |
|
||||
| **Total** | **[X]** | **[X]** | **[X]** | **[X]** | **[X]%** |
|
||||
|
||||
**Key Findings**:
|
||||
- [List any important findings, patterns, or issues discovered]
|
||||
|
||||
#### Application Tests (`ColaFlow.Application.Tests`)
|
||||
|
||||
| Test Suite | Total Tests | Passed | Failed | Skipped | Coverage |
|
||||
|------------|-------------|--------|--------|---------|----------|
|
||||
| Commands | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Queries | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Validators | [X] | [X] | [X] | [X] | [X]% |
|
||||
| Behaviors | [X] | [X] | [X] | [X] | [X]% |
|
||||
| **Total** | **[X]** | **[X]** | **[X]** | **[X]** | **[X]%** |
|
||||
|
||||
**Key Findings**:
|
||||
- [List any important findings]
|
||||
|
||||
### Integration Tests
|
||||
|
||||
#### API Tests (`ColaFlow.IntegrationTests`)
|
||||
|
||||
| Test Suite | Total Tests | Passed | Failed | Skipped | Notes |
|
||||
|------------|-------------|--------|--------|---------|-------|
|
||||
| Projects API | [X] | [X] | [X] | [X] | [Notes] |
|
||||
| Tasks API | [X] | [X] | [X] | [X] | [Notes] |
|
||||
| Workflows API | [X] | [X] | [X] | [X] | [Notes] |
|
||||
| Authentication | [X] | [X] | [X] | [X] | [Notes] |
|
||||
| **Total** | **[X]** | **[X]** | **[X]** | **[X]** | |
|
||||
|
||||
**Key Findings**:
|
||||
- [List any important findings]
|
||||
|
||||
### Test Coverage Report
|
||||
|
||||
#### Overall Coverage
|
||||
|
||||
```
|
||||
Summary:
|
||||
Generated on: [Date]
|
||||
Line coverage: [X]%
|
||||
Branch coverage: [X]%
|
||||
Method coverage: [X]%
|
||||
```
|
||||
|
||||
#### Coverage by Layer
|
||||
|
||||
| Layer | Line Coverage | Branch Coverage | Method Coverage | Status |
|
||||
|-------|---------------|-----------------|-----------------|--------|
|
||||
| Domain | [X]% | [X]% | [X]% | [✅/❌] |
|
||||
| Application | [X]% | [X]% | [X]% | [✅/❌] |
|
||||
| Infrastructure | [X]% | [X]% | [X]% | [✅/❌] |
|
||||
| API | [X]% | [X]% | [X]% | [✅/❌] |
|
||||
|
||||
#### Low Coverage Areas
|
||||
|
||||
| Component | Coverage | Priority | Action Plan |
|
||||
|-----------|----------|----------|-------------|
|
||||
| [Component name] | [X]% | [High/Medium/Low] | [Action to improve] |
|
||||
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
|
||||
### Critical Bugs (P0)
|
||||
|
||||
| Bug ID | Title | Status | Assignee | Notes |
|
||||
|--------|-------|--------|----------|-------|
|
||||
| [ID] | [Title] | [Open/Fixed/Closed] | [Name] | [Brief description] |
|
||||
|
||||
**Total**: [X]
|
||||
|
||||
### High Priority Bugs (P1)
|
||||
|
||||
| Bug ID | Title | Status | Assignee | Notes |
|
||||
|--------|-------|--------|----------|-------|
|
||||
| [ID] | [Title] | [Open/Fixed/Closed] | [Name] | [Brief description] |
|
||||
|
||||
**Total**: [X]
|
||||
|
||||
### Medium Priority Bugs (P2)
|
||||
|
||||
| Bug ID | Title | Status | Assignee | Notes |
|
||||
|--------|-------|--------|----------|-------|
|
||||
| [ID] | [Title] | [Open/Fixed/Closed] | [Name] | [Brief description] |
|
||||
|
||||
**Total**: [X]
|
||||
|
||||
### Low Priority Bugs (P3)
|
||||
|
||||
| Bug ID | Title | Status | Assignee | Notes |
|
||||
|--------|-------|--------|----------|-------|
|
||||
| [ID] | [Title] | [Open/Fixed/Closed] | [Name] | [Brief description] |
|
||||
|
||||
**Total**: [X]
|
||||
|
||||
---
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Docker Environment Status
|
||||
|
||||
| Service | Status | Port | Health Check | Notes |
|
||||
|---------|--------|------|--------------|-------|
|
||||
| PostgreSQL | [✅/❌] | 5432 | [Passing/Failing] | [Notes] |
|
||||
| Redis | [✅/❌] | 6379 | [Passing/Failing] | [Notes] |
|
||||
| Backend API | [✅/❌] | 5000 | [Passing/Failing] | [Notes] |
|
||||
| Frontend | [✅/❌] | 3000 | [Passing/Failing] | [Notes] |
|
||||
|
||||
**Issues**:
|
||||
- [List any environment setup issues]
|
||||
|
||||
### Testcontainers Status
|
||||
|
||||
| Container | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| PostgreSQL Test | [✅/❌] | [Notes] |
|
||||
| Redis Test | [✅/❌] | [Notes] |
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
### Test Frameworks & Tools
|
||||
|
||||
| Tool | Version | Status | Notes |
|
||||
|------|---------|--------|-------|
|
||||
| xUnit | [X.X.X] | [✅/❌] | [Notes] |
|
||||
| FluentAssertions | [X.X.X] | [✅/❌] | [Notes] |
|
||||
| Moq | [X.X.X] | [✅/❌] | [Notes] |
|
||||
| Testcontainers | [X.X.X] | [✅/❌] | [Notes] |
|
||||
| Coverlet | [X.X.X] | [✅/❌] | [Notes] |
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
| Pipeline | Status | Last Run | Duration | Notes |
|
||||
|----------|--------|----------|----------|-------|
|
||||
| Test Workflow | [✅/❌] | [Date/Time] | [X]min | [Notes] |
|
||||
| Coverage Workflow | [✅/❌] | [Date/Time] | [X]min | [Notes] |
|
||||
| Docker Build | [✅/❌] | [Date/Time] | [X]min | [Notes] |
|
||||
|
||||
---
|
||||
|
||||
## Test Metrics & Trends
|
||||
|
||||
### Test Execution Time
|
||||
|
||||
| Test Type | Total Tests | Avg Time/Test | Total Time |
|
||||
|-----------|-------------|---------------|------------|
|
||||
| Unit Tests | [X] | [X]ms | [X]s |
|
||||
| Integration Tests | [X] | [X]ms | [X]s |
|
||||
| **Total** | **[X]** | **[X]ms** | **[X]s** |
|
||||
|
||||
### Historical Comparison
|
||||
|
||||
| Metric | Sprint 0 | Sprint 1 | Change | Trend |
|
||||
|--------|----------|----------|--------|-------|
|
||||
| Total Tests | [X] | [X] | +[X] | [↑/↓/→] |
|
||||
| Line Coverage | [X]% | [X]% | +[X]% | [↑/↓/→] |
|
||||
| Bugs Found | [X] | [X] | +[X] | [↑/↓/→] |
|
||||
|
||||
---
|
||||
|
||||
## Blockers & Risks
|
||||
|
||||
### Current Blockers
|
||||
|
||||
| ID | Description | Impact | Workaround | Owner | ETA |
|
||||
|----|-------------|--------|------------|-------|-----|
|
||||
| [X] | [Description] | [High/Medium/Low] | [Workaround if any] | [Name] | [Date] |
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Probability | Impact | Mitigation Strategy |
|
||||
|------|-------------|--------|---------------------|
|
||||
| [Risk description] | [High/Medium/Low] | [High/Medium/Low] | [Strategy] |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions Required
|
||||
|
||||
1. [Action item with priority and owner]
|
||||
2. [Action item with priority and owner]
|
||||
3. [Action item with priority and owner]
|
||||
|
||||
### Improvements for Next Sprint
|
||||
|
||||
1. [Improvement suggestion]
|
||||
2. [Improvement suggestion]
|
||||
3. [Improvement suggestion]
|
||||
|
||||
### Technical Debt
|
||||
|
||||
| Item | Priority | Effort | Notes |
|
||||
|------|----------|--------|-------|
|
||||
| [Technical debt item] | [High/Medium/Low] | [S/M/L] | [Notes] |
|
||||
|
||||
---
|
||||
|
||||
## Test Artifacts
|
||||
|
||||
### Generated Reports
|
||||
|
||||
- [Link to HTML coverage report]
|
||||
- [Link to test results (TRX files)]
|
||||
- [Link to CI/CD pipeline run]
|
||||
|
||||
### Test Data
|
||||
|
||||
- [Link to test data seeds]
|
||||
- [Link to sample payloads]
|
||||
|
||||
### Screenshots/Videos
|
||||
|
||||
- [Link to any relevant screenshots or recordings]
|
||||
|
||||
---
|
||||
|
||||
## Approval
|
||||
|
||||
### QA Sign-off
|
||||
|
||||
- **QA Engineer**: [Name]
|
||||
- **Date**: [Date]
|
||||
- **Recommendation**: [Approve for Release / Needs Fixes / Blocked]
|
||||
|
||||
### Comments
|
||||
|
||||
[Any additional comments or observations]
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### Test Cases Executed
|
||||
|
||||
[Optional: Detailed list of test cases if needed]
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
[Optional: Detailed environment settings if needed]
|
||||
|
||||
### Known Issues
|
||||
|
||||
[Optional: List of known issues that are being tracked]
|
||||
|
||||
---
|
||||
|
||||
**Report Generated**: [Date/Time]
|
||||
**Generated By**: [QA Tool/Manual]
|
||||
**Version**: 1.0
|
||||
15
colaflow-api/tests/TestContainers.config.json
Normal file
15
colaflow-api/tests/TestContainers.config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/testcontainers.json",
|
||||
"testcontainers": {
|
||||
"version": "3.9.0",
|
||||
"resourceReaperEnabled": true,
|
||||
"hubImageNamePrefix": "",
|
||||
"ryukPrivileged": false,
|
||||
"ryukDisabled": false
|
||||
},
|
||||
"docker": {
|
||||
"host": "npipe://./pipe/docker_engine",
|
||||
"socketOverride": "",
|
||||
"certPath": ""
|
||||
}
|
||||
}
|
||||
151
colaflow-api/tests/WebApplicationFactoryBase.cs
Normal file
151
colaflow-api/tests/WebApplicationFactoryBase.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Testcontainers.Redis;
|
||||
|
||||
namespace ColaFlow.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Custom WebApplicationFactory for API integration tests
|
||||
/// Replaces production database with Testcontainers
|
||||
/// </summary>
|
||||
/// <typeparam name="TProgram">Program class from ColaFlow.API</typeparam>
|
||||
/// <typeparam name="TDbContext">DbContext class from ColaFlow.Infrastructure</typeparam>
|
||||
public class ColaFlowWebApplicationFactory<TProgram, TDbContext>
|
||||
: WebApplicationFactory<TProgram>, IAsyncDisposable
|
||||
where TProgram : class
|
||||
where TDbContext : DbContext
|
||||
{
|
||||
private PostgreSqlContainer? _postgresContainer;
|
||||
private RedisContainer? _redisContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Configure services for testing
|
||||
/// </summary>
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureServices(async services =>
|
||||
{
|
||||
// Remove existing DbContext registration
|
||||
var dbContextDescriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(DbContextOptions<TDbContext>));
|
||||
|
||||
if (dbContextDescriptor != null)
|
||||
{
|
||||
services.Remove(dbContextDescriptor);
|
||||
}
|
||||
|
||||
// Start Testcontainers
|
||||
_postgresContainer = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
.WithDatabase("colaflow_test")
|
||||
.WithUsername("colaflow_test")
|
||||
.WithPassword("colaflow_test_password")
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
_redisContainer = new RedisBuilder()
|
||||
.WithImage("redis:7-alpine")
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
await _postgresContainer.StartAsync();
|
||||
await _redisContainer.StartAsync();
|
||||
|
||||
// Add test DbContext with Testcontainers connection string
|
||||
services.AddDbContext<TDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(_postgresContainer.GetConnectionString());
|
||||
options.EnableSensitiveDataLogging();
|
||||
options.EnableDetailedErrors();
|
||||
});
|
||||
|
||||
// Replace Redis connection string
|
||||
// TODO: Configure Redis connection with Testcontainers connection string
|
||||
|
||||
// Build service provider and apply migrations
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TDbContext>();
|
||||
|
||||
// Apply migrations
|
||||
await dbContext.Database.MigrateAsync();
|
||||
});
|
||||
|
||||
// Optional: Disable HTTPS redirection for tests
|
||||
builder.UseEnvironment("Testing");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new scope with fresh DbContext
|
||||
/// </summary>
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
return Services.CreateScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get DbContext from a scope
|
||||
/// </summary>
|
||||
public TDbContext GetDbContext(IServiceScope scope)
|
||||
{
|
||||
return scope.ServiceProvider.GetRequiredService<TDbContext>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup Testcontainers
|
||||
/// </summary>
|
||||
public new async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_postgresContainer != null)
|
||||
{
|
||||
await _postgresContainer.DisposeAsync();
|
||||
}
|
||||
|
||||
if (_redisContainer != null)
|
||||
{
|
||||
await _redisContainer.DisposeAsync();
|
||||
}
|
||||
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example usage in test class:
|
||||
///
|
||||
/// public class ProjectsApiTests : IClassFixture<ColaFlowWebApplicationFactory<Program, ColaFlowDbContext>>
|
||||
/// {
|
||||
/// private readonly HttpClient _client;
|
||||
/// private readonly ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> _factory;
|
||||
///
|
||||
/// public ProjectsApiTests(ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> factory)
|
||||
/// {
|
||||
/// _factory = factory;
|
||||
/// _client = factory.CreateClient();
|
||||
/// }
|
||||
///
|
||||
/// [Fact]
|
||||
/// public async Task GetProjects_ReturnsSuccessStatusCode()
|
||||
/// {
|
||||
/// // Arrange
|
||||
/// using var scope = _factory.CreateScope();
|
||||
/// var dbContext = _factory.GetDbContext(scope);
|
||||
///
|
||||
/// // Seed test data
|
||||
/// // ...
|
||||
///
|
||||
/// // Act
|
||||
/// var response = await _client.GetAsync("/api/v1/projects");
|
||||
///
|
||||
/// // Assert
|
||||
/// response.EnsureSuccessStatusCode();
|
||||
/// }
|
||||
/// }
|
||||
/// </summary>
|
||||
Reference in New Issue
Block a user