diff --git a/docs/tests/unit/lombok-project-boilerplate.md b/docs/tests/unit/lombok-project-boilerplate.md new file mode 100644 index 0000000..6fd7bef --- /dev/null +++ b/docs/tests/unit/lombok-project-boilerplate.md @@ -0,0 +1,117 @@ +# Test Evidence: Targeted Project Lombok boilerplate + +- **Test type:** Temporary source audit (removed after GREEN) +- **Requirement IDs:** `ARC-002`, `ARC-005`, `ARC-006`, `TST-001` +- **Scenario IDs:** `AC-TST-001` +- **Test class/method:** N/A; the temporary source audit was removed after its RED/GREEN cycle +- **Implementation commit:** `e5639c1` + +## Protected behavior + +Project Spring components use targeted required-argument constructor generation only when their +constructors assign required final dependencies. Project JPA entities use only protected no-arg +constructor generation. Records remain records, and entity identity, lazy associations, explicit +domain accessors, aggregate constructors, and mutation methods do not gain broad generated APIs. + +## Temporary RED/GREEN method + +The temporary source-contract test inspected only `feature.project` production sources. It required +`@RequiredArgsConstructor` on the three injection-only components, removal of the stateless advice's +handwritten no-arg constructor, and protected `@NoArgsConstructor` on the three JPA entities. It also +rejected broad entity Lombok annotations, confirmed representative explicit domain APIs remained, +and verified every Project immutable DTO/value type remained a Java record. It was deleted after +preserving the historical RED/GREEN below because exact imports, annotation spelling, and source +substrings are implementation details rather than a durable public contract. + +## Hand-derived expected result + +Seven handwritten constructors are mechanical and eligible for removal: three dependency-assignment +constructors, one empty advice constructor, and three empty protected JPA constructors. The three +entity domain constructors, all aggregate mutation methods, defensive-copy accessors, derived +membership/leadership accessors, validation constructors, exception constructors, and all thirteen +records must remain explicit or remain records because they carry behavior or preserve the existing +API shape. + +## RED + +**Command** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH" +./mvnw -Dtest=ProjectLombokBoilerplateTest test +``` + +**Observed result** + +```text +[INFO] Running com.lab.labtimesheet.feature.project.repository.ProjectLombokBoilerplateTest +[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 +ProjectLombokBoilerplateTest.eligibleConstructorsUseTargetedLombokWithoutChangingDomainApis +expected ProjectController.java to contain import lombok.RequiredArgsConstructor; and +@RequiredArgsConstructor, but neither was present and the handwritten assignment-only constructor +remained. +[INFO] BUILD FAILURE +``` + +## GREEN + +**Command** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH" +./mvnw -Dtest=ProjectLombokBoilerplateTest test +``` + +**Observed result** + +```text +[INFO] Running com.lab.labtimesheet.feature.project.repository.ProjectLombokBoilerplateTest +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS +``` + +## Affected suite + +**Command and result** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH" +./mvnw -DskipTests compile +[INFO] BUILD SUCCESS + +./mvnw -Dtest=ProjectEntityTest,ProjectPersistenceStructureTest,ProjectTaskMutationContextTest test +[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS + +./mvnw -Dtest=ProjectControllerTest test +[INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS + +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +./mvnw -Dtest=ProjectServiceIntegrationTest test +[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS + +./mvnw -Dtest='Project*Test' test +[INFO] Tests run: 44, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS + +./mvnw -DskipTests -Dmaven.javadoc.failOnWarnings=true -Ddoclint=all \ + -Dsubpackages=com.lab.labtimesheet.feature.project javadoc:javadoc +[INFO] BUILD SUCCESS + +git diff --check +(no output; exit 0) +``` + +## External-test boundaries + +The removed source audit did not prove Lombok annotation processing, Spring constructor injection, +Hibernate materialization, PostgreSQL mappings, Thymeleaf behavior, Project authorization, locking, +or aggregate lifecycle rules. Those durable boundaries are covered by the compile, scoped +Javadoc/doclint, Project unit/web, and PostgreSQL 18.4 integration gates above. The first sandboxed +unit-suite attempt could not attach Mockito's Byte Buddy agent; the unchanged command passed after +approved execution outside that sandbox. Browser E2E behavior remains outside this unit milestone. diff --git a/src/main/java/com/lab/labtimesheet/feature/project/controller/ProjectController.java b/src/main/java/com/lab/labtimesheet/feature/project/controller/ProjectController.java index e11f370..feacced 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/controller/ProjectController.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/controller/ProjectController.java @@ -8,6 +8,7 @@ import com.lab.labtimesheet.feature.project.service.ProjectQueryService; import com.lab.labtimesheet.feature.project.service.ProjectService; import jakarta.validation.Valid; import java.security.Principal; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -27,22 +28,12 @@ import org.springframework.web.bind.annotation.RequestMapping; */ @Controller @RequestMapping("/projects") +@RequiredArgsConstructor public class ProjectController { private final ProjectQueryService pages; private final ProjectService projects; - /** - * Creates the MVC adapter for Project queries and mutations. - * - * @param pages authorized Project read operations - * @param projects transactional Project mutation operations - */ - public ProjectController(ProjectQueryService pages, ProjectService projects) { - this.pages = pages; - this.projects = projects; - } - /** * Lists only Projects visible to the authenticated actor and exposes Project creation only * to Mentors. diff --git a/src/main/java/com/lab/labtimesheet/feature/project/exception/ProjectControllerAdvice.java b/src/main/java/com/lab/labtimesheet/feature/project/exception/ProjectControllerAdvice.java index 3fa2f45..3730d30 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/exception/ProjectControllerAdvice.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/exception/ProjectControllerAdvice.java @@ -17,10 +17,6 @@ import org.springframework.web.servlet.ModelAndView; @ControllerAdvice(assignableTypes = ProjectController.class) public class ProjectControllerAdvice { - /** Creates the stateless Project exception-to-view adapter. */ - public ProjectControllerAdvice() { - } - /** * Hides whether a requested Project or nested resource exists. * diff --git a/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectEntity.java b/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectEntity.java index 6a3ed22..877a309 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectEntity.java @@ -22,6 +22,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA aggregate root for Project lifecycle, membership intervals, and leadership intervals. @@ -32,6 +34,7 @@ import java.util.Set; */ @Entity @Table(name = "projects") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProjectEntity { @Id @@ -75,10 +78,6 @@ public class ProjectEntity { @Version private long version; - /** Constructor reserved for JPA materialization. */ - protected ProjectEntity() { - } - private ProjectEntity( long mentorUserId, String name, diff --git a/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectLeadershipTermEntity.java b/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectLeadershipTermEntity.java index dbfd7ec..367f51f 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectLeadershipTermEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectLeadershipTermEntity.java @@ -11,6 +11,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.time.Instant; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA leadership interval attached to an active same-Project membership. @@ -20,6 +22,7 @@ import java.time.Instant; */ @Entity @Table(name = "project_leadership_terms") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProjectLeadershipTermEntity { @Id @@ -46,10 +49,6 @@ public class ProjectLeadershipTermEntity { @Column(name = "ended_by_mentor_user_id") private Long endedByMentorUserId; - /** Constructor reserved for JPA materialization. */ - protected ProjectLeadershipTermEntity() { - } - ProjectLeadershipTermEntity( ProjectEntity project, ProjectMembershipEntity membership, diff --git a/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectMembershipEntity.java b/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectMembershipEntity.java index 3240d17..de75340 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectMembershipEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/model/entity/ProjectMembershipEntity.java @@ -11,6 +11,8 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Version; import java.time.Instant; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA membership interval linking one Intern to one Project. @@ -20,6 +22,7 @@ import java.time.Instant; */ @Entity @Table(name = "project_memberships") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProjectMembershipEntity { @Id @@ -48,10 +51,6 @@ public class ProjectMembershipEntity { @Version private long version; - /** Constructor reserved for JPA materialization. */ - protected ProjectMembershipEntity() { - } - ProjectMembershipEntity(ProjectEntity project, long internUserId, Instant joinedAt, long addedByUserId) { this.project = project; this.internUserId = internUserId; diff --git a/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectQueryService.java b/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectQueryService.java index 8bc861c..2d11096 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectQueryService.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectQueryService.java @@ -15,6 +15,7 @@ import com.lab.labtimesheet.feature.project.model.dto.ProjectTaskMemberView; import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity; import com.lab.labtimesheet.feature.project.repository.ProjectRepository; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,22 +27,12 @@ import org.springframework.transaction.annotation.Transactional; * members but expose no current Leader or active-member context. */ @Service +@RequiredArgsConstructor public class ProjectQueryService { private final ProjectRepository projects; private final AccountService accounts; - /** - * Creates the Project read service. - * - * @param projects Project aggregate repository - * @param accounts public Account identity and internship-eligibility boundary - */ - public ProjectQueryService(ProjectRepository projects, AccountService accounts) { - this.projects = projects; - this.accounts = accounts; - } - /** * Resolves an active authenticated account to its stable user identifier. * diff --git a/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectService.java b/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectService.java index 8295e2e..c20489c 100644 --- a/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectService.java +++ b/src/main/java/com/lab/labtimesheet/feature/project/service/ProjectService.java @@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.task.service.TaskQueryService; import java.time.Clock; import java.util.Set; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; * imports their repositories or entities. */ @Service +@RequiredArgsConstructor public class ProjectService { private final ProjectRepository projects; @@ -30,28 +32,6 @@ public class ProjectService { private final TaskQueryService taskQueries; private final Clock clock; - /** - * Creates the Project mutation service. - * - * @param projects Project aggregate repository - * @param accounts public Account identity and eligibility boundary - * @param queries DTO-only Project query boundary reused for locked Task context - * @param taskQueries public Task activation-guard boundary - * @param clock server clock supplying persisted mutation instants - */ - public ProjectService( - ProjectRepository projects, - AccountService accounts, - ProjectQueryService queries, - TaskQueryService taskQueries, - Clock clock) { - this.projects = projects; - this.accounts = accounts; - this.queries = queries; - this.taskQueries = taskQueries; - this.clock = clock; - } - /** * Atomically creates a planned Mentor-owned Project, eligible initial membership, and first * leadership term. {@code saveAndFlush} exposes database invariant violations before commit.