Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
332
colaflow-api/tests/ColaFlow.Domain.Tests/Aggregates/EpicTests.cs
Normal file
332
colaflow-api/tests/ColaFlow.Domain.Tests/Aggregates/EpicTests.cs
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,226 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Domain Events
|
||||
/// </summary>
|
||||
public class DomainEventsTests
|
||||
{
|
||||
#region ProjectCreatedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectCreatedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var projectName = "Test Project";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
|
||||
// Assert
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.ProjectName.Should().Be(projectName);
|
||||
@event.CreatedBy.Should().Be(createdBy);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectCreatedEvent_ShouldBeRecord()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var projectName = "Test Project";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var event1 = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
var event2 = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
|
||||
// Assert - Records with same values should be equal
|
||||
event1.ProjectId.Should().Be(event2.ProjectId);
|
||||
event1.ProjectName.Should().Be(event2.ProjectName);
|
||||
event1.CreatedBy.Should().Be(event2.CreatedBy);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProjectUpdatedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectUpdatedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var name = "Updated Project";
|
||||
var description = "Updated Description";
|
||||
|
||||
// Act
|
||||
var @event = new ProjectUpdatedEvent(projectId, name, description);
|
||||
|
||||
// Assert
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.Name.Should().Be(name);
|
||||
@event.Description.Should().Be(description);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectUpdatedEvent_WithNullDescription_ShouldAcceptNull()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var name = "Updated Project";
|
||||
|
||||
// Act
|
||||
var @event = new ProjectUpdatedEvent(projectId, name, null!);
|
||||
|
||||
// Assert
|
||||
@event.Description.Should().BeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProjectArchivedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectArchivedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new ProjectArchivedEvent(projectId);
|
||||
|
||||
// Assert
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectArchivedEvent_ShouldBeRecord()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var event1 = new ProjectArchivedEvent(projectId);
|
||||
var event2 = new ProjectArchivedEvent(projectId);
|
||||
|
||||
// Assert - Records with same values should be equal
|
||||
event1.ProjectId.Should().Be(event2.ProjectId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EpicCreatedEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void EpicCreatedEvent_Constructor_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var epicId = EpicId.Create();
|
||||
var epicName = "Epic 1";
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new EpicCreatedEvent(epicId, epicName, projectId);
|
||||
|
||||
// Assert
|
||||
@event.EpicId.Should().Be(epicId);
|
||||
@event.EpicName.Should().Be(epicName);
|
||||
@event.ProjectId.Should().Be(projectId);
|
||||
@event.OccurredOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpicCreatedEvent_ShouldBeRecord()
|
||||
{
|
||||
// Arrange
|
||||
var epicId = EpicId.Create();
|
||||
var epicName = "Epic 1";
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Act
|
||||
var event1 = new EpicCreatedEvent(epicId, epicName, projectId);
|
||||
var event2 = new EpicCreatedEvent(epicId, epicName, projectId);
|
||||
|
||||
// Assert - Records with same values should be equal
|
||||
event1.EpicId.Should().Be(event2.EpicId);
|
||||
event1.EpicName.Should().Be(event2.EpicName);
|
||||
event1.ProjectId.Should().Be(event2.ProjectId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Timing Tests
|
||||
|
||||
[Fact]
|
||||
public void DomainEvents_OccurredOn_ShouldBeUtcTime()
|
||||
{
|
||||
// Arrange & Act
|
||||
var projectCreatedEvent = new ProjectCreatedEvent(ProjectId.Create(), "Test", UserId.Create());
|
||||
var projectUpdatedEvent = new ProjectUpdatedEvent(ProjectId.Create(), "Test", "Desc");
|
||||
var projectArchivedEvent = new ProjectArchivedEvent(ProjectId.Create());
|
||||
var epicCreatedEvent = new EpicCreatedEvent(EpicId.Create(), "Epic", ProjectId.Create());
|
||||
|
||||
// Assert
|
||||
projectCreatedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
projectUpdatedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
projectArchivedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
epicCreatedEvent.OccurredOn.Kind.Should().Be(DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DomainEvents_OccurredOn_ShouldBeSetAutomatically()
|
||||
{
|
||||
// Arrange
|
||||
var beforeCreation = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
var @event = new ProjectCreatedEvent(ProjectId.Create(), "Test", UserId.Create());
|
||||
|
||||
// Assert
|
||||
var afterCreation = DateTime.UtcNow;
|
||||
@event.OccurredOn.Should().BeOnOrAfter(beforeCreation);
|
||||
@event.OccurredOn.Should().BeOnOrBefore(afterCreation);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Immutability Tests
|
||||
|
||||
[Fact]
|
||||
public void DomainEvents_ShouldBeImmutable()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = ProjectId.Create();
|
||||
var projectName = "Test Project";
|
||||
var createdBy = UserId.Create();
|
||||
|
||||
// Act
|
||||
var @event = new ProjectCreatedEvent(projectId, projectName, createdBy);
|
||||
var originalProjectId = @event.ProjectId;
|
||||
var originalProjectName = @event.ProjectName;
|
||||
var originalCreatedBy = @event.CreatedBy;
|
||||
var originalOccurredOn = @event.OccurredOn;
|
||||
|
||||
// Try to access properties multiple times
|
||||
var projectId1 = @event.ProjectId;
|
||||
var projectName1 = @event.ProjectName;
|
||||
var createdBy1 = @event.CreatedBy;
|
||||
var occurredOn1 = @event.OccurredOn;
|
||||
|
||||
// Assert - Properties should not change
|
||||
projectId1.Should().Be(originalProjectId);
|
||||
projectName1.Should().Be(originalProjectName);
|
||||
createdBy1.Should().Be(originalCreatedBy);
|
||||
occurredOn1.Should().Be(originalOccurredOn);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Enumeration-based value objects
|
||||
/// </summary>
|
||||
public class EnumerationTests
|
||||
{
|
||||
#region ProjectStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Active_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
ProjectStatus.Active.Id.Should().Be(1);
|
||||
ProjectStatus.Active.Name.Should().Be("Active");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Archived_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
ProjectStatus.Archived.Id.Should().Be(2);
|
||||
ProjectStatus.Archived.Name.Should().Be("Archived");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_OnHold_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
ProjectStatus.OnHold.Id.Should().Be(3);
|
||||
ProjectStatus.OnHold.Name.Should().Be("On Hold");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Equals_WithSameStatus_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = ProjectStatus.Active;
|
||||
var status2 = ProjectStatus.Active;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().Be(status2);
|
||||
status1.Equals(status2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_Equals_WithDifferentStatus_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = ProjectStatus.Active;
|
||||
var status2 = ProjectStatus.Archived;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().NotBe(status2);
|
||||
status1.Equals(status2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectStatus_ToString_ShouldReturnName()
|
||||
{
|
||||
// Arrange
|
||||
var status = ProjectStatus.Active;
|
||||
|
||||
// Act
|
||||
var result = status.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Active");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WorkItemStatus Tests
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_ToDo_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.ToDo.Id.Should().Be(1);
|
||||
WorkItemStatus.ToDo.Name.Should().Be("To Do");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_InProgress_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.InProgress.Id.Should().Be(2);
|
||||
WorkItemStatus.InProgress.Name.Should().Be("In Progress");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_InReview_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.InReview.Id.Should().Be(3);
|
||||
WorkItemStatus.InReview.Name.Should().Be("In Review");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Done_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.Done.Id.Should().Be(4);
|
||||
WorkItemStatus.Done.Name.Should().Be("Done");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Blocked_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
WorkItemStatus.Blocked.Id.Should().Be(5);
|
||||
WorkItemStatus.Blocked.Name.Should().Be("Blocked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Equals_WithSameStatus_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = WorkItemStatus.ToDo;
|
||||
var status2 = WorkItemStatus.ToDo;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().Be(status2);
|
||||
status1.Equals(status2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_Equals_WithDifferentStatus_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var status1 = WorkItemStatus.ToDo;
|
||||
var status2 = WorkItemStatus.Done;
|
||||
|
||||
// Act & Assert
|
||||
status1.Should().NotBe(status2);
|
||||
status1.Equals(status2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkItemStatus_ToString_ShouldReturnName()
|
||||
{
|
||||
// Arrange
|
||||
var status = WorkItemStatus.InProgress;
|
||||
|
||||
// Act
|
||||
var result = status.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be("In Progress");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TaskPriority Tests
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Low_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.Low.Id.Should().Be(1);
|
||||
TaskPriority.Low.Name.Should().Be("Low");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Medium_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.Medium.Id.Should().Be(2);
|
||||
TaskPriority.Medium.Name.Should().Be("Medium");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_High_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.High.Id.Should().Be(3);
|
||||
TaskPriority.High.Name.Should().Be("High");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Urgent_ShouldHaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
TaskPriority.Urgent.Id.Should().Be(4);
|
||||
TaskPriority.Urgent.Name.Should().Be("Urgent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Equals_WithSamePriority_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var priority1 = TaskPriority.High;
|
||||
var priority2 = TaskPriority.High;
|
||||
|
||||
// Act & Assert
|
||||
priority1.Should().Be(priority2);
|
||||
priority1.Equals(priority2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_Equals_WithDifferentPriority_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var priority1 = TaskPriority.Low;
|
||||
var priority2 = TaskPriority.Urgent;
|
||||
|
||||
// Act & Assert
|
||||
priority1.Should().NotBe(priority2);
|
||||
priority1.Equals(priority2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_ToString_ShouldReturnName()
|
||||
{
|
||||
// Arrange
|
||||
var priority = TaskPriority.Medium;
|
||||
|
||||
// Act
|
||||
var result = priority.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Medium");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskPriority_ShouldBeOrderedByImportance()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
TaskPriority.Low.Id.Should().BeLessThan(TaskPriority.Medium.Id);
|
||||
TaskPriority.Medium.Id.Should().BeLessThan(TaskPriority.High.Id);
|
||||
TaskPriority.High.Id.Should().BeLessThan(TaskPriority.Urgent.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ProjectId value object
|
||||
/// </summary>
|
||||
public class ProjectIdTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var projectId = ProjectId.Create();
|
||||
|
||||
// Assert
|
||||
projectId.Should().NotBeNull();
|
||||
projectId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithGuid_ShouldCreateProjectIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var projectId = ProjectId.Create(guid);
|
||||
|
||||
// Assert
|
||||
projectId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void From_WithGuid_ShouldCreateProjectId()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var projectId = ProjectId.From(guid);
|
||||
|
||||
// Assert
|
||||
projectId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_MultipleCalls_ShouldGenerateDifferentGuids()
|
||||
{
|
||||
// Act
|
||||
var projectId1 = ProjectId.Create();
|
||||
var projectId2 = ProjectId.Create();
|
||||
|
||||
// Assert
|
||||
projectId1.Value.Should().NotBe(projectId2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId1 = ProjectId.Create(guid);
|
||||
var projectId2 = ProjectId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
projectId1.Should().Be(projectId2);
|
||||
projectId1.Equals(projectId2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithDifferentValue_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var projectId1 = ProjectId.Create();
|
||||
var projectId2 = ProjectId.Create();
|
||||
|
||||
// Act & Assert
|
||||
projectId1.Should().NotBe(projectId2);
|
||||
projectId1.Equals(projectId2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithSameValue_ShouldReturnSameHashCode()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId1 = ProjectId.Create(guid);
|
||||
var projectId2 = ProjectId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
projectId1.GetHashCode().Should().Be(projectId2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_ShouldReturnGuidString()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId = ProjectId.Create(guid);
|
||||
|
||||
// Act
|
||||
var result = projectId.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be(guid.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueObject_ShouldBeImmutable()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId = ProjectId.Create(guid);
|
||||
var originalValue = projectId.Value;
|
||||
|
||||
// Act - Try to get the value multiple times
|
||||
var value1 = projectId.Value;
|
||||
var value2 = projectId.Value;
|
||||
|
||||
// Assert
|
||||
value1.Should().Be(originalValue);
|
||||
value2.Should().Be(originalValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ProjectKey value object
|
||||
/// </summary>
|
||||
public class ProjectKeyTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithValidKey_ShouldCreateProjectKey()
|
||||
{
|
||||
// Arrange
|
||||
var key = "COLA";
|
||||
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Should().NotBeNull();
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("A")]
|
||||
[InlineData("AB")]
|
||||
[InlineData("ABC")]
|
||||
[InlineData("ABCD")]
|
||||
[InlineData("ABCDEFGHIJ")] // 10 characters - max allowed
|
||||
public void Create_WithValidLengthKeys_ShouldSucceed(string key)
|
||||
{
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("TEST")]
|
||||
[InlineData("COLA")]
|
||||
[InlineData("FLOW")]
|
||||
[InlineData("PROJECT1")]
|
||||
[InlineData("ABC123")]
|
||||
public void Create_WithUppercaseLettersAndNumbers_ShouldSucceed(string key)
|
||||
{
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyKey_ShouldThrowDomainException(string invalidKey)
|
||||
{
|
||||
// Act
|
||||
Action act = () => ProjectKey.Create(invalidKey);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project key cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithKeyExceeding10Characters_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var key = "ABCDEFGHIJK"; // 11 characters
|
||||
|
||||
// Act
|
||||
Action act = () => ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project key cannot exceed 10 characters");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("test")] // lowercase
|
||||
[InlineData("Test")] // mixed case
|
||||
[InlineData("TEST-1")] // hyphen
|
||||
[InlineData("TEST_1")] // underscore
|
||||
[InlineData("TEST 1")] // space
|
||||
[InlineData("TEST.1")] // dot
|
||||
[InlineData("TEST@1")] // special char
|
||||
public void Create_WithInvalidCharacters_ShouldThrowDomainException(string invalidKey)
|
||||
{
|
||||
// Act
|
||||
Action act = () => ProjectKey.Create(invalidKey);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project key must contain only uppercase letters and numbers");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var projectKey1 = ProjectKey.Create(key);
|
||||
var projectKey2 = ProjectKey.Create(key);
|
||||
|
||||
// Act & Assert
|
||||
projectKey1.Should().Be(projectKey2);
|
||||
projectKey1.Equals(projectKey2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithDifferentValue_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var projectKey1 = ProjectKey.Create("TEST1");
|
||||
var projectKey2 = ProjectKey.Create("TEST2");
|
||||
|
||||
// Act & Assert
|
||||
projectKey1.Should().NotBe(projectKey2);
|
||||
projectKey1.Equals(projectKey2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithSameValue_ShouldReturnSameHashCode()
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var projectKey1 = ProjectKey.Create(key);
|
||||
var projectKey2 = ProjectKey.Create(key);
|
||||
|
||||
// Act & Assert
|
||||
projectKey1.GetHashCode().Should().Be(projectKey2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_ShouldReturnKeyValue()
|
||||
{
|
||||
// Arrange
|
||||
var key = "COLA";
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Act
|
||||
var result = projectKey.ToString();
|
||||
|
||||
// Assert
|
||||
result.Should().Be(key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueObject_ShouldBeImmutable()
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
var originalValue = projectKey.Value;
|
||||
|
||||
// Act - Try to get the value multiple times
|
||||
var value1 = projectKey.Value;
|
||||
var value2 = projectKey.Value;
|
||||
|
||||
// Assert
|
||||
value1.Should().Be(originalValue);
|
||||
value2.Should().Be(originalValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("123")]
|
||||
[InlineData("789")]
|
||||
[InlineData("1234567890")]
|
||||
public void Create_WithOnlyNumbers_ShouldSucceed(string key)
|
||||
{
|
||||
// Act
|
||||
var projectKey = ProjectKey.Create(key);
|
||||
|
||||
// Assert
|
||||
projectKey.Value.Should().Be(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for strongly-typed ID value objects (EpicId, StoryId, TaskId, UserId)
|
||||
/// </summary>
|
||||
public class StronglyTypedIdTests
|
||||
{
|
||||
#region EpicId Tests
|
||||
|
||||
[Fact]
|
||||
public void EpicId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var epicId = EpicId.Create();
|
||||
|
||||
// Assert
|
||||
epicId.Should().NotBeNull();
|
||||
epicId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpicId_Create_WithGuid_ShouldCreateEpicIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var epicId = EpicId.Create(guid);
|
||||
|
||||
// Assert
|
||||
epicId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpicId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var epicId1 = EpicId.Create(guid);
|
||||
var epicId2 = EpicId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
epicId1.Should().Be(epicId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StoryId Tests
|
||||
|
||||
[Fact]
|
||||
public void StoryId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var storyId = StoryId.Create();
|
||||
|
||||
// Assert
|
||||
storyId.Should().NotBeNull();
|
||||
storyId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoryId_Create_WithGuid_ShouldCreateStoryIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var storyId = StoryId.Create(guid);
|
||||
|
||||
// Assert
|
||||
storyId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoryId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var storyId1 = StoryId.Create(guid);
|
||||
var storyId2 = StoryId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
storyId1.Should().Be(storyId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TaskId Tests
|
||||
|
||||
[Fact]
|
||||
public void TaskId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var taskId = TaskId.Create();
|
||||
|
||||
// Assert
|
||||
taskId.Should().NotBeNull();
|
||||
taskId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskId_Create_WithGuid_ShouldCreateTaskIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var taskId = TaskId.Create(guid);
|
||||
|
||||
// Assert
|
||||
taskId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var taskId1 = TaskId.Create(guid);
|
||||
var taskId2 = TaskId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
taskId1.Should().Be(taskId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UserId Tests
|
||||
|
||||
[Fact]
|
||||
public void UserId_Create_WithoutParameter_ShouldGenerateNewGuid()
|
||||
{
|
||||
// Act
|
||||
var userId = UserId.Create();
|
||||
|
||||
// Assert
|
||||
userId.Should().NotBeNull();
|
||||
userId.Value.Should().NotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserId_Create_WithGuid_ShouldCreateUserIdWithGivenValue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var userId = UserId.Create(guid);
|
||||
|
||||
// Assert
|
||||
userId.Value.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserId_Equals_WithSameValue_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var userId1 = UserId.Create(guid);
|
||||
var userId2 = UserId.Create(guid);
|
||||
|
||||
// Act & Assert
|
||||
userId1.Should().Be(userId2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Safety Tests
|
||||
|
||||
[Fact]
|
||||
public void DifferentIdTypes_WithSameGuid_ShouldNotBeEqual()
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var projectId = ProjectId.Create(guid);
|
||||
var epicId = EpicId.Create(guid);
|
||||
|
||||
// Act & Assert - They are different types, so should not be equal
|
||||
projectId.Should().NotBe((object)epicId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleCalls_ShouldGenerateDifferentGuids()
|
||||
{
|
||||
// Act
|
||||
var id1 = ProjectId.Create();
|
||||
var id2 = ProjectId.Create();
|
||||
var id3 = EpicId.Create();
|
||||
var id4 = StoryId.Create();
|
||||
var id5 = TaskId.Create();
|
||||
|
||||
// Assert
|
||||
id1.Value.Should().NotBe(id2.Value);
|
||||
id1.Value.Should().NotBe(id3.Value);
|
||||
id1.Value.Should().NotBe(id4.Value);
|
||||
id1.Value.Should().NotBe(id5.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user