From 4468e7b8919c8e68f1e92eca2d17196094d600a0 Mon Sep 17 00:00:00 2001 From: Robert van Diest Date: Fri, 27 Mar 2026 17:13:55 +0100 Subject: [PATCH] refactor(domain): introduce UserFactory and WorkplaceFactory for reconstitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistent with ReservationFactory — moves Reconstitute out of the entity into a dedicated factory class, and makes the reconstitution constructor internal to restrict direct instantiation to within the domain assembly. Adds unit tests for all three factories. Co-Authored-By: Claude Sonnet 4.6 --- src/backend/src/Randall.Domain/Users/User.cs | 5 +- .../src/Randall.Domain/Users/UserFactory.cs | 7 +++ .../Randall.Domain/Workplaces/Workplace.cs | 5 +- .../Workplaces/WorkplaceFactory.cs | 7 +++ .../Persistence/Mappers/UserMapper.cs | 2 +- .../Persistence/Mappers/WorkplaceMapper.cs | 2 +- .../Reservations/ReservationFactoryTests.cs | 61 +++++++++++++++++++ .../Users/UserFactoryTests.cs | 58 ++++++++++++++++++ .../Workplaces/WorkplaceFactoryTests.cs | 45 ++++++++++++++ 9 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 src/backend/src/Randall.Domain/Users/UserFactory.cs create mode 100644 src/backend/src/Randall.Domain/Workplaces/WorkplaceFactory.cs create mode 100644 src/backend/tests/unit/Randall.Domain.UnitTests/Reservations/ReservationFactoryTests.cs create mode 100644 src/backend/tests/unit/Randall.Domain.UnitTests/Users/UserFactoryTests.cs create mode 100644 src/backend/tests/unit/Randall.Domain.UnitTests/Workplaces/WorkplaceFactoryTests.cs diff --git a/src/backend/src/Randall.Domain/Users/User.cs b/src/backend/src/Randall.Domain/Users/User.cs index 422b796..bef69c5 100644 --- a/src/backend/src/Randall.Domain/Users/User.cs +++ b/src/backend/src/Randall.Domain/Users/User.cs @@ -10,10 +10,7 @@ public class User : Entity public bool IsApproved { get; private set; } public bool IsAdmin { get; private set; } - public static User Reconstitute(Guid id, string email, string name, string passwordHash, bool isApproved, bool isAdmin) => - new(id, email, name, passwordHash, isApproved, isAdmin); - - private User(Guid id, string email, string name, string passwordHash, bool isApproved, bool isAdmin) : base(id) + internal User(Guid id, string email, string name, string passwordHash, bool isApproved, bool isAdmin) : base(id) { Email = email; Name = name; diff --git a/src/backend/src/Randall.Domain/Users/UserFactory.cs b/src/backend/src/Randall.Domain/Users/UserFactory.cs new file mode 100644 index 0000000..517b19c --- /dev/null +++ b/src/backend/src/Randall.Domain/Users/UserFactory.cs @@ -0,0 +1,7 @@ +namespace Randall.Domain.Users; + +public static class UserFactory +{ + public static User Reconstitute(Guid id, string email, string name, string passwordHash, bool isApproved, bool isAdmin) => + new(id, email, name, passwordHash, isApproved, isAdmin); +} diff --git a/src/backend/src/Randall.Domain/Workplaces/Workplace.cs b/src/backend/src/Randall.Domain/Workplaces/Workplace.cs index eca5e77..10ff22e 100644 --- a/src/backend/src/Randall.Domain/Workplaces/Workplace.cs +++ b/src/backend/src/Randall.Domain/Workplaces/Workplace.cs @@ -8,10 +8,7 @@ public class Workplace : Entity public string Location { get; private set; } public bool IsActive { get; private set; } - public static Workplace Reconstitute(Guid id, string name, string location, bool isActive) => - new(id, name, location, isActive); - - private Workplace(Guid id, string name, string location, bool isActive) : base(id) + internal Workplace(Guid id, string name, string location, bool isActive) : base(id) { Name = name; Location = location; diff --git a/src/backend/src/Randall.Domain/Workplaces/WorkplaceFactory.cs b/src/backend/src/Randall.Domain/Workplaces/WorkplaceFactory.cs new file mode 100644 index 0000000..815a82d --- /dev/null +++ b/src/backend/src/Randall.Domain/Workplaces/WorkplaceFactory.cs @@ -0,0 +1,7 @@ +namespace Randall.Domain.Workplaces; + +public static class WorkplaceFactory +{ + public static Workplace Reconstitute(Guid id, string name, string location, bool isActive) => + new(id, name, location, isActive); +} diff --git a/src/backend/src/Randall.Infrastructure/Persistence/Mappers/UserMapper.cs b/src/backend/src/Randall.Infrastructure/Persistence/Mappers/UserMapper.cs index 979f71a..075e6e7 100644 --- a/src/backend/src/Randall.Infrastructure/Persistence/Mappers/UserMapper.cs +++ b/src/backend/src/Randall.Infrastructure/Persistence/Mappers/UserMapper.cs @@ -6,7 +6,7 @@ namespace Randall.Infrastructure.Persistence.Mappers; public static class UserMapper { public static User ToDomain(UserRecord record) => - User.Reconstitute( + UserFactory.Reconstitute( record.Id, record.Email, record.Name, diff --git a/src/backend/src/Randall.Infrastructure/Persistence/Mappers/WorkplaceMapper.cs b/src/backend/src/Randall.Infrastructure/Persistence/Mappers/WorkplaceMapper.cs index e7af9c6..6161959 100644 --- a/src/backend/src/Randall.Infrastructure/Persistence/Mappers/WorkplaceMapper.cs +++ b/src/backend/src/Randall.Infrastructure/Persistence/Mappers/WorkplaceMapper.cs @@ -6,7 +6,7 @@ namespace Randall.Infrastructure.Persistence.Mappers; public static class WorkplaceMapper { public static Workplace ToDomain(WorkplaceRecord record) => - Workplace.Reconstitute( + WorkplaceFactory.Reconstitute( record.Id, record.Name, record.Location, diff --git a/src/backend/tests/unit/Randall.Domain.UnitTests/Reservations/ReservationFactoryTests.cs b/src/backend/tests/unit/Randall.Domain.UnitTests/Reservations/ReservationFactoryTests.cs new file mode 100644 index 0000000..4930c1a --- /dev/null +++ b/src/backend/tests/unit/Randall.Domain.UnitTests/Reservations/ReservationFactoryTests.cs @@ -0,0 +1,61 @@ +using Randall.Domain.Reservations; + +namespace Randall.Domain.UnitTests.Reservations; + +public class ReservationFactoryTests +{ + private static readonly Guid Id = Guid.NewGuid(); + private static readonly Guid WorkplaceId = Guid.NewGuid(); + private const string Email = "jane@company.com"; + private const string Name = "Jane Smith"; + private static readonly DateOnly Date = new(2026, 6, 1); + private static readonly DateTime CreatedAt = new(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc); + + [Fact] + public void Reconstitute_MapsAllFieldsExactly() + { + var reservation = ReservationFactory.Reconstitute(Id, WorkplaceId, Email, Name, Date, ReservationStatus.Active, CreatedAt); + + Assert.Equal(Id, reservation.Id); + Assert.Equal(WorkplaceId, reservation.WorkplaceId); + Assert.Equal(Email, reservation.EmployeeEmail); + Assert.Equal(Name, reservation.EmployeeName); + Assert.Equal(Date, reservation.Date); + Assert.Equal(ReservationStatus.Active, reservation.Status); + Assert.Equal(CreatedAt, reservation.CreatedAt); + } + + [Fact] + public void Reconstitute_PreservesProvidedId() + { + var reservation = ReservationFactory.Reconstitute(Id, WorkplaceId, Email, Name, Date, ReservationStatus.Active, CreatedAt); + + Assert.Equal(Id, reservation.Id); + } + + [Fact] + public void Reconstitute_CanRestoreActiveReservation() + { + var reservation = ReservationFactory.Reconstitute(Id, WorkplaceId, Email, Name, Date, ReservationStatus.Active, CreatedAt); + + Assert.Equal(ReservationStatus.Active, reservation.Status); + } + + [Fact] + public void Reconstitute_CanRestoreCancelledReservation() + { + var reservation = ReservationFactory.Reconstitute(Id, WorkplaceId, Email, Name, Date, ReservationStatus.Cancelled, CreatedAt); + + Assert.Equal(ReservationStatus.Cancelled, reservation.Status); + } + + [Fact] + public void Reconstitute_CanRestorePastReservation() + { + var pastDate = new DateOnly(2020, 1, 1); + + var reservation = ReservationFactory.Reconstitute(Id, WorkplaceId, Email, Name, pastDate, ReservationStatus.Active, CreatedAt); + + Assert.Equal(pastDate, reservation.Date); + } +} diff --git a/src/backend/tests/unit/Randall.Domain.UnitTests/Users/UserFactoryTests.cs b/src/backend/tests/unit/Randall.Domain.UnitTests/Users/UserFactoryTests.cs new file mode 100644 index 0000000..abebbf6 --- /dev/null +++ b/src/backend/tests/unit/Randall.Domain.UnitTests/Users/UserFactoryTests.cs @@ -0,0 +1,58 @@ +using Randall.Domain.Users; + +namespace Randall.Domain.UnitTests.Users; + +public class UserFactoryTests +{ + private static readonly Guid Id = Guid.NewGuid(); + private const string Email = "jane@company.com"; + private const string Name = "Jane Smith"; + private const string PasswordHash = "hashed-password"; + + [Fact] + public void Reconstitute_MapsAllFieldsExactly() + { + var user = UserFactory.Reconstitute(Id, Email, Name, PasswordHash, isApproved: true, isAdmin: false); + + Assert.Equal(Id, user.Id); + Assert.Equal(Email, user.Email); + Assert.Equal(Name, user.Name); + Assert.Equal(PasswordHash, user.PasswordHash); + Assert.True(user.IsApproved); + Assert.False(user.IsAdmin); + } + + [Fact] + public void Reconstitute_PreservesProvidedId() + { + var user = UserFactory.Reconstitute(Id, Email, Name, PasswordHash, isApproved: false, isAdmin: false); + + Assert.Equal(Id, user.Id); + } + + [Fact] + public void Reconstitute_CanRestoreApprovedNonAdminUser() + { + var user = UserFactory.Reconstitute(Id, Email, Name, PasswordHash, isApproved: true, isAdmin: false); + + Assert.True(user.IsApproved); + Assert.False(user.IsAdmin); + } + + [Fact] + public void Reconstitute_CanRestoreUnapprovedUser() + { + var user = UserFactory.Reconstitute(Id, Email, Name, PasswordHash, isApproved: false, isAdmin: false); + + Assert.False(user.IsApproved); + } + + [Fact] + public void Reconstitute_CanRestoreAdminUser() + { + var user = UserFactory.Reconstitute(Id, Email, Name, PasswordHash, isApproved: true, isAdmin: true); + + Assert.True(user.IsAdmin); + Assert.True(user.IsApproved); + } +} diff --git a/src/backend/tests/unit/Randall.Domain.UnitTests/Workplaces/WorkplaceFactoryTests.cs b/src/backend/tests/unit/Randall.Domain.UnitTests/Workplaces/WorkplaceFactoryTests.cs new file mode 100644 index 0000000..9198812 --- /dev/null +++ b/src/backend/tests/unit/Randall.Domain.UnitTests/Workplaces/WorkplaceFactoryTests.cs @@ -0,0 +1,45 @@ +using Randall.Domain.Workplaces; + +namespace Randall.Domain.UnitTests.Workplaces; + +public class WorkplaceFactoryTests +{ + private static readonly Guid Id = Guid.NewGuid(); + private const string Name = "D13"; + private const string Location = "Pod A"; + + [Fact] + public void Reconstitute_MapsAllFieldsExactly() + { + var workplace = WorkplaceFactory.Reconstitute(Id, Name, Location, isActive: true); + + Assert.Equal(Id, workplace.Id); + Assert.Equal(Name, workplace.Name); + Assert.Equal(Location, workplace.Location); + Assert.True(workplace.IsActive); + } + + [Fact] + public void Reconstitute_PreservesProvidedId() + { + var workplace = WorkplaceFactory.Reconstitute(Id, Name, Location, isActive: true); + + Assert.Equal(Id, workplace.Id); + } + + [Fact] + public void Reconstitute_CanRestoreActiveWorkplace() + { + var workplace = WorkplaceFactory.Reconstitute(Id, Name, Location, isActive: true); + + Assert.True(workplace.IsActive); + } + + [Fact] + public void Reconstitute_CanRestoreInactiveWorkplace() + { + var workplace = WorkplaceFactory.Reconstitute(Id, Name, Location, isActive: false); + + Assert.False(workplace.IsActive); + } +}