feat(projects): enforce iteration 1 project lifecycle
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package com.lab.labtimesheet.projects.domain;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ProjectTest {
|
||||
|
||||
private static final Instant CREATED_AT = Instant.parse("2026-08-14T02:00:00Z");
|
||||
|
||||
@Test
|
||||
void planningCreatesTheInitialLeaderMembershipAndTermTogether() {
|
||||
var project = Project.plan(
|
||||
10L,
|
||||
" Intern Portal Refresh ",
|
||||
" Refresh the portal ",
|
||||
LocalDate.of(2026, 8, 15),
|
||||
LocalDate.of(2026, 9, 30),
|
||||
activeIntern(20L),
|
||||
CREATED_AT);
|
||||
|
||||
assertEquals(ProjectStatus.PLANNED, project.status());
|
||||
assertEquals("Intern Portal Refresh", project.name());
|
||||
assertEquals("Refresh the portal", project.description());
|
||||
assertEquals(1, project.memberships().size());
|
||||
assertEquals(20L, project.memberships().getFirst().internUserId());
|
||||
assertEquals(10L, project.memberships().getFirst().addedByUserId());
|
||||
assertEquals(1, project.leadershipTerms().size());
|
||||
assertEquals(20L, project.currentLeader().internUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void planningRejectsAnIneligibleInitialLeaderAndInvalidDates() {
|
||||
assertThrows(ProjectRuleViolation.class, () -> Project.plan(
|
||||
10L,
|
||||
"Project",
|
||||
null,
|
||||
LocalDate.of(2026, 9, 1),
|
||||
LocalDate.of(2026, 8, 31),
|
||||
activeIntern(20L),
|
||||
CREATED_AT));
|
||||
assertThrows(ProjectRuleViolation.class, () -> Project.plan(
|
||||
10L,
|
||||
"Project",
|
||||
null,
|
||||
LocalDate.of(2026, 8, 1),
|
||||
LocalDate.of(2026, 8, 31),
|
||||
new EligibleIntern(20L, false, true),
|
||||
CREATED_AT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerAddsEligibleMembersButNotDuplicateCurrentMemberships() {
|
||||
var project = plannedProject();
|
||||
|
||||
project.addMember(10L, activeIntern(21L), CREATED_AT.plusSeconds(60));
|
||||
|
||||
assertEquals(2, project.memberships().size());
|
||||
assertTrue(project.hasCurrentMember(21L));
|
||||
assertThrows(ProjectRuleViolation.class,
|
||||
() -> project.addMember(10L, activeIntern(21L), CREATED_AT.plusSeconds(120)));
|
||||
assertThrows(ProjectAccessDenied.class,
|
||||
() -> project.addMember(11L, activeIntern(22L), CREATED_AT.plusSeconds(120)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void theSameInternCanBelongToSeparateProjects() {
|
||||
var first = plannedProject();
|
||||
var second = Project.plan(
|
||||
11L,
|
||||
"Second",
|
||||
null,
|
||||
LocalDate.of(2026, 8, 15),
|
||||
LocalDate.of(2026, 9, 30),
|
||||
activeIntern(21L),
|
||||
CREATED_AT);
|
||||
|
||||
first.addMember(10L, activeIntern(21L), CREATED_AT.plusSeconds(60));
|
||||
|
||||
assertTrue(first.hasCurrentMember(21L));
|
||||
assertTrue(second.hasCurrentMember(21L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerChangesExactlyOneLeaderWithoutChangingMemberships() {
|
||||
var project = plannedProject();
|
||||
project.addMember(10L, activeIntern(21L), CREATED_AT.plusSeconds(60));
|
||||
|
||||
project.changeLeader(10L, activeIntern(21L), CREATED_AT.plusSeconds(120));
|
||||
|
||||
assertEquals(2, project.memberships().size());
|
||||
assertEquals(2, project.leadershipTerms().size());
|
||||
assertEquals(1, project.leadershipTerms().stream().filter(LeadershipTerm::isCurrent).count());
|
||||
assertEquals(21L, project.currentLeader().internUserId());
|
||||
assertFalse(project.leadershipTerms().getFirst().isCurrent());
|
||||
assertThrows(ProjectRuleViolation.class,
|
||||
() -> project.changeLeader(10L, activeIntern(21L), CREATED_AT.plusSeconds(180)));
|
||||
assertThrows(ProjectRuleViolation.class,
|
||||
() -> project.changeLeader(10L, activeIntern(22L), CREATED_AT.plusSeconds(180)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activationRequiresOwnerAndValidCurrentTaskAssignees() {
|
||||
var project = plannedProject();
|
||||
|
||||
assertThrows(ProjectAccessDenied.class,
|
||||
() -> project.activate(11L, true, CREATED_AT.plusSeconds(60)));
|
||||
assertThrows(ProjectRuleViolation.class,
|
||||
() -> project.activate(10L, false, CREATED_AT.plusSeconds(60)));
|
||||
|
||||
project.activate(10L, true, CREATED_AT.plusSeconds(60));
|
||||
|
||||
assertEquals(ProjectStatus.ACTIVE, project.status());
|
||||
assertEquals(CREATED_AT.plusSeconds(60), project.activatedAt());
|
||||
assertThrows(ProjectRuleViolation.class,
|
||||
() -> project.activate(10L, true, CREATED_AT.plusSeconds(120)));
|
||||
}
|
||||
|
||||
private static Project plannedProject() {
|
||||
return Project.plan(
|
||||
10L,
|
||||
"Project",
|
||||
null,
|
||||
LocalDate.of(2026, 8, 15),
|
||||
LocalDate.of(2026, 9, 30),
|
||||
activeIntern(20L),
|
||||
CREATED_AT);
|
||||
}
|
||||
|
||||
private static EligibleIntern activeIntern(long userId) {
|
||||
return new EligibleIntern(userId, true, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user