From ed293a57be6fa03dfe24f30c3e825731640a17fe Mon Sep 17 00:00:00 2001 From: Robert van Diest Date: Wed, 25 Mar 2026 19:22:25 +0100 Subject: [PATCH] Add unittests --- .claude/settings.local.json | 3 +- .github/workflows/ci.yml | 18 ++++ src/backend/Randall.slnx | 4 + src/backend/tests/unit/Common/ResultTests.cs | 53 ++++++++++ .../unit/Randall.Domain.UnitTests.csproj | 25 +++++ .../Reservations/ReservationCancelTests.cs | 48 +++++++++ .../Reservations/ReservationCreateTests.cs | 98 +++++++++++++++++++ .../tests/unit/Users/UserCreateTests.cs | 84 ++++++++++++++++ .../tests/unit/Users/UserStateTests.cs | 42 ++++++++ .../tests/unit/Workplaces/WorkplaceTests.cs | 61 ++++++++++++ 10 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 src/backend/tests/unit/Common/ResultTests.cs create mode 100644 src/backend/tests/unit/Randall.Domain.UnitTests.csproj create mode 100644 src/backend/tests/unit/Reservations/ReservationCancelTests.cs create mode 100644 src/backend/tests/unit/Reservations/ReservationCreateTests.cs create mode 100644 src/backend/tests/unit/Users/UserCreateTests.cs create mode 100644 src/backend/tests/unit/Users/UserStateTests.cs create mode 100644 src/backend/tests/unit/Workplaces/WorkplaceTests.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 934ac94..fedfde7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -11,7 +11,8 @@ "Bash(npm create:*)", "Bash(npm install:*)", "Bash(npm run:*)", - "Bash(npx playwright:*)" + "Bash(npx playwright:*)", + "Bash(dotnet test:*)" ] } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88e1352..cc14f64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,28 @@ on: pull_request: jobs: + # ── Unit Tests ───────────────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Run unit tests + run: dotnet test src/backend/tests/unit/Randall.Domain.UnitTests.csproj --logger "console;verbosity=normal" + # ── E2E Tests ────────────────────────────────────────────────────────────── e2e: name: E2E Tests runs-on: ubuntu-latest + needs: unit-tests steps: - name: Checkout diff --git a/src/backend/Randall.slnx b/src/backend/Randall.slnx index 5e21be4..51d8016 100644 --- a/src/backend/Randall.slnx +++ b/src/backend/Randall.slnx @@ -5,4 +5,8 @@ + + + + diff --git a/src/backend/tests/unit/Common/ResultTests.cs b/src/backend/tests/unit/Common/ResultTests.cs new file mode 100644 index 0000000..51eed99 --- /dev/null +++ b/src/backend/tests/unit/Common/ResultTests.cs @@ -0,0 +1,53 @@ +using Randall.Domain.Common; + +namespace Randall.Domain.UnitTests.Common; + +public class ResultTests +{ + [Fact] + public void Success_IsSuccessIsTrue() + { + var result = Result.Success(); + + Assert.True(result.IsSuccess); + Assert.Null(result.Error); + } + + [Fact] + public void Failure_IsSuccessIsFalse() + { + var result = Result.Failure("Something went wrong"); + + Assert.False(result.IsSuccess); + Assert.Equal("Something went wrong", result.Error); + } + + [Fact] + public void SuccessOfT_ContainsValue() + { + var result = Result.Success(42); + + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + Assert.Null(result.Error); + } + + [Fact] + public void FailureOfT_ValueIsDefault() + { + var result = Result.Failure("error"); + + Assert.False(result.IsSuccess); + Assert.Equal(default, result.Value); + Assert.Equal("error", result.Error); + } + + [Fact] + public void FailureOfT_ReferenceType_ValueIsNull() + { + var result = Result.Failure("error"); + + Assert.False(result.IsSuccess); + Assert.Null(result.Value); + } +} diff --git a/src/backend/tests/unit/Randall.Domain.UnitTests.csproj b/src/backend/tests/unit/Randall.Domain.UnitTests.csproj new file mode 100644 index 0000000..d3517dd --- /dev/null +++ b/src/backend/tests/unit/Randall.Domain.UnitTests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/tests/unit/Reservations/ReservationCancelTests.cs b/src/backend/tests/unit/Reservations/ReservationCancelTests.cs new file mode 100644 index 0000000..9edae56 --- /dev/null +++ b/src/backend/tests/unit/Reservations/ReservationCancelTests.cs @@ -0,0 +1,48 @@ +using Randall.Domain.Reservations; + +namespace Randall.Domain.UnitTests.Reservations; + +public class ReservationCancelTests +{ + private static Reservation CreateActiveReservation() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + return Reservation.Create(Guid.NewGuid(), "jane@company.com", "Jane", today, today).Value!; + } + + [Fact] + public void Cancel_ActiveReservation_Succeeds() + { + var reservation = CreateActiveReservation(); + + var result = reservation.Cancel(); + + Assert.True(result.IsSuccess); + Assert.Equal(ReservationStatus.Cancelled, reservation.Status); + } + + [Fact] + public void Cancel_AlreadyCancelledReservation_Fails() + { + var reservation = CreateActiveReservation(); + reservation.Cancel(); + + var result = reservation.Cancel(); + + Assert.False(result.IsSuccess); + Assert.Contains("already cancelled", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Cancel_DoesNotChangeOtherFields() + { + var reservation = CreateActiveReservation(); + var originalDate = reservation.Date; + var originalEmail = reservation.EmployeeEmail; + + reservation.Cancel(); + + Assert.Equal(originalDate, reservation.Date); + Assert.Equal(originalEmail, reservation.EmployeeEmail); + } +} diff --git a/src/backend/tests/unit/Reservations/ReservationCreateTests.cs b/src/backend/tests/unit/Reservations/ReservationCreateTests.cs new file mode 100644 index 0000000..3e74151 --- /dev/null +++ b/src/backend/tests/unit/Reservations/ReservationCreateTests.cs @@ -0,0 +1,98 @@ +using Randall.Domain.Reservations; + +namespace Randall.Domain.UnitTests.Reservations; + +public class ReservationCreateTests +{ + private static readonly Guid WorkplaceId = Guid.NewGuid(); + private const string Email = "jane@company.com"; + private const string Name = "Jane Smith"; + + [Fact] + public void Create_WithValidFutureDate_Succeeds() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var result = Reservation.Create(WorkplaceId, Email, Name, today, today); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [Fact] + public void Create_WithTodayAsDate_Succeeds() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var result = Reservation.Create(WorkplaceId, Email, Name, today, today); + + Assert.True(result.IsSuccess); + } + + [Fact] + public void Create_WithMaxAdvanceDate_Succeeds() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var maxDate = today.AddDays(Reservation.MaxAdvanceDays); + + var result = Reservation.Create(WorkplaceId, Email, Name, maxDate, today); + + Assert.True(result.IsSuccess); + } + + [Fact] + public void Create_WithPastDate_Fails() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var yesterday = today.AddDays(-1); + + var result = Reservation.Create(WorkplaceId, Email, Name, yesterday, today); + + Assert.False(result.IsSuccess); + Assert.Contains("past", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Create_BeyondMaxAdvanceDays_Fails() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var tooFar = today.AddDays(Reservation.MaxAdvanceDays + 1); + + var result = Reservation.Create(WorkplaceId, Email, Name, tooFar, today); + + Assert.False(result.IsSuccess); + Assert.Contains(Reservation.MaxAdvanceDays.ToString(), result.Error); + } + + [Fact] + public void Create_SetsStatusToActive() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var result = Reservation.Create(WorkplaceId, Email, Name, today, today); + + Assert.Equal(ReservationStatus.Active, result.Value!.Status); + } + + [Fact] + public void Create_PopulatesAllFields() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var result = Reservation.Create(WorkplaceId, Email, Name, today, today); + + var reservation = result.Value!; + Assert.Equal(WorkplaceId, reservation.WorkplaceId); + Assert.Equal(Email, reservation.EmployeeEmail); + Assert.Equal(Name, reservation.EmployeeName); + Assert.Equal(today, reservation.Date); + Assert.NotEqual(Guid.Empty, reservation.Id); + } + + [Fact] + public void Create_SetsCreatedAtToApproximatelyNow() + { + var before = DateTime.UtcNow; + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var result = Reservation.Create(WorkplaceId, Email, Name, today, today); + var after = DateTime.UtcNow; + + Assert.InRange(result.Value!.CreatedAt, before, after); + } +} diff --git a/src/backend/tests/unit/Users/UserCreateTests.cs b/src/backend/tests/unit/Users/UserCreateTests.cs new file mode 100644 index 0000000..359048b --- /dev/null +++ b/src/backend/tests/unit/Users/UserCreateTests.cs @@ -0,0 +1,84 @@ +using Randall.Domain.Users; + +namespace Randall.Domain.UnitTests.Users; + +public class UserCreateTests +{ + [Fact] + public void Create_WithValidData_Succeeds() + { + var result = User.Create("jane@company.com", "Jane Smith", "hash"); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [Fact] + public void Create_NormalisesEmailToLowercase() + { + var result = User.Create("JANE@COMPANY.COM", "Jane Smith", "hash"); + + Assert.Equal("jane@company.com", result.Value!.Email); + } + + [Fact] + public void Create_TrimsNameWhitespace() + { + var result = User.Create("jane@company.com", " Jane Smith ", "hash"); + + Assert.Equal("Jane Smith", result.Value!.Name); + } + + [Fact] + public void Create_WithEmptyEmail_Fails() + { + var result = User.Create("", "Jane Smith", "hash"); + + Assert.False(result.IsSuccess); + Assert.Contains("email", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Create_WithWhitespaceEmail_Fails() + { + var result = User.Create(" ", "Jane Smith", "hash"); + + Assert.False(result.IsSuccess); + } + + [Fact] + public void Create_WithEmptyName_Fails() + { + var result = User.Create("jane@company.com", "", "hash"); + + Assert.False(result.IsSuccess); + Assert.Contains("name", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Create_RegularUser_IsNotApprovedByDefault() + { + var result = User.Create("jane@company.com", "Jane Smith", "hash"); + + Assert.False(result.Value!.IsApproved); + Assert.False(result.Value!.IsAdmin); + } + + [Fact] + public void Create_AdminUser_IsAutoApproved() + { + var result = User.Create("admin@company.com", "Admin", "hash", isAdmin: true); + + Assert.True(result.Value!.IsAdmin); + Assert.True(result.Value!.IsApproved); + } + + [Fact] + public void Create_AssignsUniqueId() + { + var a = User.Create("a@company.com", "A", "hash").Value!; + var b = User.Create("b@company.com", "B", "hash").Value!; + + Assert.NotEqual(a.Id, b.Id); + } +} diff --git a/src/backend/tests/unit/Users/UserStateTests.cs b/src/backend/tests/unit/Users/UserStateTests.cs new file mode 100644 index 0000000..141f0a7 --- /dev/null +++ b/src/backend/tests/unit/Users/UserStateTests.cs @@ -0,0 +1,42 @@ +using Randall.Domain.Users; + +namespace Randall.Domain.UnitTests.Users; + +public class UserStateTests +{ + private static User CreateRegularUser() => + User.Create("jane@company.com", "Jane Smith", "hash").Value!; + + [Fact] + public void Approve_SetsIsApprovedToTrue() + { + var user = CreateRegularUser(); + + user.Approve(); + + Assert.True(user.IsApproved); + } + + [Fact] + public void MakeAdmin_SetsIsAdminAndIsApproved() + { + var user = CreateRegularUser(); + + user.MakeAdmin(); + + Assert.True(user.IsAdmin); + Assert.True(user.IsApproved); + } + + [Fact] + public void MakeAdmin_OnAlreadyApprovedUser_RemainsApproved() + { + var user = CreateRegularUser(); + user.Approve(); + + user.MakeAdmin(); + + Assert.True(user.IsApproved); + Assert.True(user.IsAdmin); + } +} diff --git a/src/backend/tests/unit/Workplaces/WorkplaceTests.cs b/src/backend/tests/unit/Workplaces/WorkplaceTests.cs new file mode 100644 index 0000000..379b628 --- /dev/null +++ b/src/backend/tests/unit/Workplaces/WorkplaceTests.cs @@ -0,0 +1,61 @@ +using Randall.Domain.Workplaces; + +namespace Randall.Domain.UnitTests.Workplaces; + +public class WorkplaceTests +{ + [Fact] + public void Create_IsActiveByDefault() + { + var workplace = new Workplace("A1", "Pod A"); + + Assert.True(workplace.IsActive); + } + + [Fact] + public void Create_SetsNameAndLocation() + { + var workplace = new Workplace("A1", "Pod A"); + + Assert.Equal("A1", workplace.Name); + Assert.Equal("Pod A", workplace.Location); + } + + [Fact] + public void Create_AssignsNonEmptyId() + { + var workplace = new Workplace("A1", "Pod A"); + + Assert.NotEqual(Guid.Empty, workplace.Id); + } + + [Fact] + public void Deactivate_SetsIsActiveToFalse() + { + var workplace = new Workplace("A1", "Pod A"); + + workplace.Deactivate(); + + Assert.False(workplace.IsActive); + } + + [Fact] + public void Activate_AfterDeactivate_SetsIsActiveToTrue() + { + var workplace = new Workplace("A1", "Pod A"); + workplace.Deactivate(); + + workplace.Activate(); + + Assert.True(workplace.IsActive); + } + + [Fact] + public void Create_TwoWorkplaces_HaveDifferentIds() + { + var a = new Workplace("A1", "Pod A"); + var b = new Workplace("A2", "Pod A"); + + Assert.NotEqual(a.Id, b.Id); + } +}