🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
435 lines
13 KiB
C#
435 lines
13 KiB
C#
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
|
|
}
|