1
0

SprintWeekRepositoryTest.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. <?php
  2. /*
  3. * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
  4. * SPDX-License-Identifier: Apache-2.0
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * See the LICENSE file in the project root for the full license text.
  9. */
  10. declare(strict_types=1);
  11. namespace App\Tests\Repositories;
  12. use App\Domain\SprintWeek;
  13. use App\Repositories\SprintRepository;
  14. use App\Repositories\SprintWeekRepository;
  15. use App\Tests\TestCase;
  16. /**
  17. * Phase 12: per-week weekday selection drives Arbeitstage.
  18. */
  19. final class SprintWeekRepositoryTest extends TestCase
  20. {
  21. /** @return array{SprintRepository, SprintWeekRepository, int} */
  22. private function seedSprint(): array
  23. {
  24. $pdo = $this->makeDb();
  25. $sprints = new SprintRepository($pdo);
  26. $weeks = new SprintWeekRepository($pdo);
  27. $sprint = $sprints->create('S', '2026-01-05', '2026-01-30', 0.2);
  28. $sprints->materializeWeeks($sprint->id, '2026-01-05', '2026-01-30', 4);
  29. return [$sprints, $weeks, $sprint->id];
  30. }
  31. public function testMaterializeWeeksDefaultsToAllFiveDays(): void
  32. {
  33. [, $weeks, $sprintId] = $this->seedSprint();
  34. $rows = $weeks->allForSprint($sprintId);
  35. $this->assertCount(4, $rows);
  36. foreach ($rows as $w) {
  37. $this->assertSame(SprintWeek::MASK_ALL, $w->activeDaysMask);
  38. $this->assertSame(5.0, $w->maxWorkingDays);
  39. }
  40. }
  41. public function testMaterializeWeeksTrimsTrailingWeekToSprintEnd(): void
  42. {
  43. $pdo = $this->makeDb();
  44. $sprints = new SprintRepository($pdo);
  45. $weeks = new SprintWeekRepository($pdo);
  46. // Mon 2026-01-05 → Wed 2026-01-14 → 2 weeks; the trailing week stops on Wed.
  47. $sprint = $sprints->create('S', '2026-01-05', '2026-01-14', 0.2);
  48. $sprints->materializeWeeks($sprint->id, '2026-01-05', '2026-01-14', 2);
  49. $rows = $weeks->allForSprint($sprint->id);
  50. $this->assertCount(2, $rows);
  51. $this->assertSame(SprintWeek::MASK_ALL, $rows[0]->activeDaysMask);
  52. $this->assertSame(5.0, $rows[0]->maxWorkingDays);
  53. // Mo + Di + Mi = bit0|bit1|bit2 = 7, three workdays.
  54. $this->assertSame(0b00111, $rows[1]->activeDaysMask);
  55. $this->assertSame(3.0, $rows[1]->maxWorkingDays);
  56. }
  57. public function testMaterializeWeeksTrimsLeadingPartialWeekFromMidWeekStart(): void
  58. {
  59. $pdo = $this->makeDb();
  60. $sprints = new SprintRepository($pdo);
  61. $weeks = new SprintWeekRepository($pdo);
  62. // Sprint runs Wed 2026-01-07 → Fri 2026-01-23 (3 weeks).
  63. $sprint = $sprints->create('S', '2026-01-07', '2026-01-23', 0.2);
  64. $sprints->materializeWeeks($sprint->id, '2026-01-07', '2026-01-23', 3);
  65. $rows = $weeks->allForSprint($sprint->id);
  66. $this->assertCount(3, $rows);
  67. // W1: Wed–Tue spans all five Mo..Fr weekdays inside the sprint.
  68. $this->assertSame(SprintWeek::MASK_ALL, $rows[0]->activeDaysMask);
  69. // W2: same, fully interior.
  70. $this->assertSame(SprintWeek::MASK_ALL, $rows[1]->activeDaysMask);
  71. // W3 starts Wed Jan 21 — only Wed/Thu/Fri fall inside [start, end].
  72. // Mi + Do + Fr = bit2|bit3|bit4 = 28.
  73. $this->assertSame(0b11100, $rows[2]->activeDaysMask);
  74. $this->assertSame(3.0, $rows[2]->maxWorkingDays);
  75. }
  76. public function testMaterializeWeeksWeekendOnlySpanProducesEmptyMask(): void
  77. {
  78. $pdo = $this->makeDb();
  79. $sprints = new SprintRepository($pdo);
  80. $weeks = new SprintWeekRepository($pdo);
  81. // Single-day sprint on Sat 2026-01-10 → no weekdays in range.
  82. $sprint = $sprints->create('S', '2026-01-10', '2026-01-10', 0.2);
  83. $sprints->materializeWeeks($sprint->id, '2026-01-10', '2026-01-10', 1);
  84. $rows = $weeks->allForSprint($sprint->id);
  85. $this->assertCount(1, $rows);
  86. $this->assertSame(0, $rows[0]->activeDaysMask);
  87. $this->assertSame(0.0, $rows[0]->maxWorkingDays);
  88. }
  89. public function testUpdateActiveDaysWritesBothColumns(): void
  90. {
  91. [, $weeks, $sprintId] = $this->seedSprint();
  92. $first = $weeks->allForSprint($sprintId)[0];
  93. $result = $weeks->updateActiveDays($first->id, 0b01111); // drop Fr
  94. $this->assertSame(SprintWeek::MASK_ALL, $result['before']->activeDaysMask);
  95. $this->assertSame(0b01111, $result['after']->activeDaysMask);
  96. $this->assertSame(4.0, $result['after']->maxWorkingDays);
  97. // Second query hydrates the same row from disk.
  98. $reloaded = $weeks->find($first->id);
  99. $this->assertNotNull($reloaded);
  100. $this->assertSame(0b01111, $reloaded->activeDaysMask);
  101. $this->assertSame(4.0, $reloaded->maxWorkingDays);
  102. }
  103. public function testUpdateActiveDaysClampsBitsOutsideMoFr(): void
  104. {
  105. [, $weeks, $sprintId] = $this->seedSprint();
  106. $first = $weeks->allForSprint($sprintId)[0];
  107. $result = $weeks->updateActiveDays($first->id, 0xFF);
  108. $this->assertSame(SprintWeek::MASK_ALL, $result['after']->activeDaysMask);
  109. $this->assertSame(5.0, $result['after']->maxWorkingDays);
  110. }
  111. public function testUpdateActiveDaysToEmptyMaskZeroesCount(): void
  112. {
  113. [, $weeks, $sprintId] = $this->seedSprint();
  114. $first = $weeks->allForSprint($sprintId)[0];
  115. $result = $weeks->updateActiveDays($first->id, 0);
  116. $this->assertSame(0, $result['after']->activeDaysMask);
  117. $this->assertSame(0.0, $result['after']->maxWorkingDays);
  118. $this->assertSame([], $result['after']->activeDays());
  119. }
  120. public function testSyncCountAppendsWeeksWithAllDaysActive(): void
  121. {
  122. [, $weeks, $sprintId] = $this->seedSprint();
  123. $diff = $weeks->syncCount($sprintId, '2026-01-05', 6);
  124. $this->assertCount(2, $diff['added']);
  125. foreach ($diff['added'] as $w) {
  126. $this->assertSame(SprintWeek::MASK_ALL, $w->activeDaysMask);
  127. $this->assertSame(5.0, $w->maxWorkingDays);
  128. }
  129. }
  130. public function testRealignDatesRewritesOffsetsAndPreservesMask(): void
  131. {
  132. [, $weeks, $sprintId] = $this->seedSprint();
  133. // Customise week 2's mask so we can verify it survives realignment.
  134. $week2 = $weeks->allForSprint($sprintId)[1];
  135. $weeks->updateActiveDays($week2->id, 0b00111); // Mo Di Mi only
  136. // Move the sprint start one week forward.
  137. $diffs = $weeks->realignDates($sprintId, '2026-01-12');
  138. $this->assertCount(4, $diffs, 'every existing week shifted one week');
  139. $reloaded = $weeks->allForSprint($sprintId);
  140. $this->assertSame('2026-01-12', $reloaded[0]->startDate);
  141. $this->assertSame('2026-01-19', $reloaded[1]->startDate);
  142. $this->assertSame('2026-01-26', $reloaded[2]->startDate);
  143. $this->assertSame('2026-02-02', $reloaded[3]->startDate);
  144. // Mask + maxWorkingDays untouched by realignment.
  145. $this->assertSame(0b00111, $reloaded[1]->activeDaysMask);
  146. $this->assertSame(3.0, $reloaded[1]->maxWorkingDays);
  147. }
  148. public function testRealignDatesNoOpWhenAlreadyAligned(): void
  149. {
  150. [, $weeks, $sprintId] = $this->seedSprint();
  151. $diffs = $weeks->realignDates($sprintId, '2026-01-05');
  152. $this->assertSame([], $diffs, 'no UPDATEs when offsets already match');
  153. }
  154. }