Files
ColaFlow/colaflow-api/tests/ColaFlow.Domain.Tests/Aggregates/WorkTaskTests.cs
Yaojia Wang 014d62bcc2 Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 23:55:18 +01:00

463 lines
14 KiB
C#

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
}