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; /// /// Unit tests for Project aggregate root /// 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(); 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() .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() .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(); 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() .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() .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(); 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() .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(); 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() .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() .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>(); } [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 }