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; /// /// Unit tests for Story entity /// 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() .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() .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() .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() .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() .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() .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>(); } [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 }