Project Init

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-02 23:55:18 +01:00
commit 014d62bcc2
169 changed files with 28867 additions and 0 deletions

View 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
}

View File

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

View File

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

View File

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