From 7c1a26d77ffc49f60d0c49aa82be55d829901090 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:34:37 +0700 Subject: [PATCH] refactor(task): replace mechanical boilerplate with Lombok --- docs/tests/unit/lombok-task-boilerplate.md | 91 ++++++++++++++ .../task/controller/TaskController.java | 11 +- .../feature/task/model/entity/Task.java | 118 ++---------------- .../task/model/entity/TaskComment.java | 52 +------- .../task/service/TaskDashboardService.java | 13 +- .../task/service/TaskQueryService.java | 11 +- .../feature/task/service/TaskService.java | 27 +--- 7 files changed, 112 insertions(+), 211 deletions(-) create mode 100644 docs/tests/unit/lombok-task-boilerplate.md diff --git a/docs/tests/unit/lombok-task-boilerplate.md b/docs/tests/unit/lombok-task-boilerplate.md new file mode 100644 index 0000000..a466924 --- /dev/null +++ b/docs/tests/unit/lombok-task-boilerplate.md @@ -0,0 +1,91 @@ +# Test Evidence: Task Lombok boilerplate retrofit + +- **Test type:** Unit +- **Requirement IDs:** `AUTH-011`, `TSK-001`–`TSK-012`, `DB-004` +- **Scenario IDs:** `AC-TSK-001`–`AC-TSK-006`, `AC-TSK-010` +- **Test class/method:** Source audit plus existing Task unit, web, and PostgreSQL integration suites +- **Implementation commit:** `pending` + +## Protected behavior + +The Task feature uses the configured Lombok processor for mechanical dependency-injection constructors, JPA no-argument constructors, and existing entity getters. Explicit Task constructors and mutation methods remain responsible for initial state, attribution, timestamps, and workflow invariants. The retrofit must not add entity equality, hash, string, or setter behavior and must not alter the existing public getter contract. + +## Test method + +The source audit counts the eligible handwritten constructors and getter methods before and after the retrofit. Existing Task tests then exercise Spring injection, MVC binding, JPA materialization, entity getters, authorization, status transitions, comments, and Project/attendance boundaries through the same public behavior used before the source-only change. + +## Hand-derived expected result + +Four Spring components have injection-only constructors, so all four may use `@RequiredArgsConstructor`. `Task` and `TaskComment` need protected JPA no-argument constructors and may use targeted Lombok generation. The 17 existing entity getters may be generated, but `Task.deletedByMembershipId`, `Task.updatedAt`, and `Task.version` must remain without newly exposed getters. Domain constructors and `Task.changeStatus` must remain explicit. + +## RED + +**Command** + +```text +task_component_boilerplate=$(rg -n 'public (TaskController|TaskService|TaskQueryService|TaskDashboardService)\(' src/main/java/com/lab/labtimesheet/feature/task | wc -l | tr -d ' ') +task_entity_boilerplate=$(rg -n 'protected (Task|TaskComment)\(\)|public (Long|long|String|Instant|LocalDate|TaskStatus) get[A-Z][A-Za-z0-9]*\(\)' src/main/java/com/lab/labtimesheet/feature/task/model/entity | wc -l | tr -d ' ') +printf 'component_boilerplate=%s entity_boilerplate=%s\n' "$task_component_boilerplate" "$task_entity_boilerplate" +test "$task_component_boilerplate" -eq 0 -a "$task_entity_boilerplate" -eq 0 +``` + +**Observed result** + +```text +component_boilerplate=4 entity_boilerplate=19 +Exit status 1. The Task package still contained all eligible handwritten boilerplate. +``` + +## GREEN + +**Command** + +```text +task_component_boilerplate=$(rg -n 'public (TaskController|TaskService|TaskQueryService|TaskDashboardService)\(' src/main/java/com/lab/labtimesheet/feature/task | wc -l | tr -d ' ') +task_entity_boilerplate=$(rg -n 'protected (Task|TaskComment)\(\)|public (Long|long|String|Instant|LocalDate|TaskStatus) get[A-Z][A-Za-z0-9]*\(\)' src/main/java/com/lab/labtimesheet/feature/task/model/entity | wc -l | tr -d ' ') +printf 'component_boilerplate=%s entity_boilerplate=%s\n' "$task_component_boilerplate" "$task_entity_boilerplate" +test "$task_component_boilerplate" -eq 0 -a "$task_entity_boilerplate" -eq 0 + +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="$JAVA_HOME/bin:$PATH" +./mvnw -DskipTests compile + +javap -classpath target/classes -p \ + com.lab.labtimesheet.feature.task.model.entity.Task \ + com.lab.labtimesheet.feature.task.model.entity.TaskComment \ + com.lab.labtimesheet.feature.task.service.TaskService \ + com.lab.labtimesheet.feature.task.controller.TaskController +``` + +**Observed result** + +```text +component_boilerplate=0 entity_boilerplate=0 +Maven compile: BUILD SUCCESS; 127 production source files compiled with Java 25. +Bytecode inspection retained the four public component constructors, both protected JPA constructors, and all 17 existing entity getters. No getter exists for deletedByMembershipId, updatedAt, or version; domain constructors and Task.changeStatus remain explicit. +``` + +## Affected suite + +**Command and result** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="$JAVA_HOME/bin:$PATH" +./mvnw -Dtest='TaskDomainRulesTest,TaskPersistenceStructureTest,TaskQueryServiceTest,TaskDashboardServiceTest' test +# BUILD SUCCESS: 26 tests, 0 failures, 0 errors, 0 skipped. + +./mvnw -Dtest='TaskControllerTest,ProjectTaskShellContractTest,ProjectTaskFormAccessibilityWebTest' test +# BUILD SUCCESS: 28 tests, 0 failures, 0 errors, 0 skipped. + +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +./mvnw -Dtest='TaskCreationIntegrationTest,TaskMutationBoundaryTest' test +# BUILD SUCCESS against PostgreSQL 18.4: 16 tests, 0 failures, 0 errors, 0 skipped. + +./mvnw -DskipTests -Ddoclint=all javadoc:javadoc +# BUILD SUCCESS. Existing warnings were outside feature.task; the Task package emitted no warning. +``` + +## External-test boundaries + +This source audit does not prove runtime behavior by itself. The affected Task tests and compilation/Javadoc gates cover the behavior-preserving contract; no browser walkthrough or production database is required because templates, mappings, schema, and business logic are unchanged. Unprivileged sandbox attempts could not attach Mockito's Java agent or reach the host Docker socket; the same commands passed outside that sandbox, with the verified OrbStack socket supplied for Testcontainers. diff --git a/src/main/java/com/lab/labtimesheet/feature/task/controller/TaskController.java b/src/main/java/com/lab/labtimesheet/feature/task/controller/TaskController.java index 572a623..65c3598 100644 --- a/src/main/java/com/lab/labtimesheet/feature/task/controller/TaskController.java +++ b/src/main/java/com/lab/labtimesheet/feature/task/controller/TaskController.java @@ -12,6 +12,7 @@ import com.lab.labtimesheet.feature.task.service.TaskService; import jakarta.validation.Valid; import java.util.Arrays; import java.util.Locale; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -31,19 +32,11 @@ import org.springframework.web.bind.annotation.RequestParam; * successful mutations use redirects to prevent duplicate submissions. */ @Controller +@RequiredArgsConstructor public class TaskController { private final TaskService taskService; - /** - * Creates the MVC adapter for the Task application service. - * - * @param taskService authorized Task use cases - */ - public TaskController(TaskService taskService) { - this.taskService = taskService; - } - @GetMapping("/projects/{projectId}/tasks") String list(Authentication authentication, @PathVariable long projectId, Model model) { TaskListView taskList = taskService.list(authentication.getName(), projectId); diff --git a/src/main/java/com/lab/labtimesheet/feature/task/model/entity/Task.java b/src/main/java/com/lab/labtimesheet/feature/task/model/entity/Task.java index 9d4c8cc..a5f6ebf 100644 --- a/src/main/java/com/lab/labtimesheet/feature/task/model/entity/Task.java +++ b/src/main/java/com/lab/labtimesheet/feature/task/model/entity/Task.java @@ -12,6 +12,9 @@ import jakarta.persistence.Table; import jakarta.persistence.Version; import java.time.Instant; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * Persisted Task aggregate row with one current same-Project assignee. @@ -22,6 +25,8 @@ import java.time.LocalDate; */ @Entity @Table(name = "tasks") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Task { @Id @@ -60,20 +65,20 @@ public class Task { private Instant deletedAt; @Column(name = "deleted_by_membership_id") + @Getter(AccessLevel.NONE) private Long deletedByMembershipId; @Column(name = "created_at", nullable = false) private Instant createdAt; @Column(name = "updated_at", nullable = false) + @Getter(AccessLevel.NONE) private Instant updatedAt; @Version + @Getter(AccessLevel.NONE) private long version; - /** Constructor reserved for JPA materialization. */ - protected Task() {} - /** * Creates a TODO Task and records the creating membership as both creator and assigner. * @@ -121,111 +126,4 @@ public class Task { updatedAt = now; } - /** - * Returns the persistence identity. - * - * @return Task identifier, or {@code null} before insertion - */ - public Long getId() { - return id; - } - - /** - * Returns the aggregate identity. - * - * @return owning Project identifier - */ - public long getProjectId() { - return projectId; - } - - /** - * Returns the current assignment identity. - * - * @return current same-Project assignee membership identifier - */ - public long getAssigneeMembershipId() { - return assigneeMembershipId; - } - - /** - * Returns the display title. - * - * @return normalized Task title - */ - public String getTitle() { - return title; - } - - /** - * Returns the descriptive text. - * - * @return optional normalized description - */ - public String getDescription() { - return description; - } - - /** - * Returns the workflow state. - * - * @return current fixed workflow status - */ - public TaskStatus getStatus() { - return status; - } - - /** - * Returns the business deadline. - * - * @return optional validated due date - */ - public LocalDate getDueDate() { - return dueDate; - } - - /** - * Returns current assignment timing. - * - * @return instant when the current assignment was established - */ - public Instant getAssignedAt() { - return assignedAt; - } - - /** - * Returns original creator attribution. - * - * @return immutable creating membership identifier - */ - public long getCreatorMembershipId() { - return creatorMembershipId; - } - - /** - * Returns current assignment attribution. - * - * @return membership identifier responsible for the current assignment - */ - public long getAssignerMembershipId() { - return assignerMembershipId; - } - - /** - * Returns lifecycle visibility state. - * - * @return soft-deletion instant, or {@code null} while current - */ - public Instant getDeletedAt() { - return deletedAt; - } - - /** - * Returns creation timing. - * - * @return immutable creation instant - */ - public Instant getCreatedAt() { - return createdAt; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/task/model/entity/TaskComment.java b/src/main/java/com/lab/labtimesheet/feature/task/model/entity/TaskComment.java index 5de025b..c4994ac 100644 --- a/src/main/java/com/lab/labtimesheet/feature/task/model/entity/TaskComment.java +++ b/src/main/java/com/lab/labtimesheet/feature/task/model/entity/TaskComment.java @@ -7,6 +7,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.time.Instant; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * Persisted append-only Task comment. @@ -16,6 +19,8 @@ import java.time.Instant; */ @Entity @Table(name = "task_comments") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class TaskComment { @Id @@ -34,9 +39,6 @@ public class TaskComment { @Column(name = "created_at", nullable = false) private Instant createdAt; - /** Constructor reserved for JPA materialization. */ - protected TaskComment() {} - /** * Creates an immutable comment from server-authorized values. * @@ -52,48 +54,4 @@ public class TaskComment { this.createdAt = createdAt; } - /** - * Returns the persistence identity. - * - * @return comment identifier, or {@code null} before insertion - */ - public Long getId() { - return id; - } - - /** - * Returns the owning record identity. - * - * @return owning Task identifier - */ - public long getTaskId() { - return taskId; - } - - /** - * Returns historical authorship. - * - * @return immutable historical author user identifier - */ - public long getAuthorUserId() { - return authorUserId; - } - - /** - * Returns comment content. - * - * @return normalized comment text - */ - public String getBody() { - return body; - } - - /** - * Returns creation timing. - * - * @return immutable creation instant - */ - public Instant getCreatedAt() { - return createdAt; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/task/service/TaskDashboardService.java b/src/main/java/com/lab/labtimesheet/feature/task/service/TaskDashboardService.java index 3a06dd8..1696131 100644 --- a/src/main/java/com/lab/labtimesheet/feature/task/service/TaskDashboardService.java +++ b/src/main/java/com/lab/labtimesheet/feature/task/service/TaskDashboardService.java @@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.task.repository.TaskRepository; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; * priorities cover current assignments only where the actor still has an active membership. */ @Service +@RequiredArgsConstructor public class TaskDashboardService { private static final TaskDashboardView EMPTY_DASHBOARD = new TaskDashboardView(0, 0, List.of()); @@ -30,17 +32,6 @@ public class TaskDashboardService { private final TaskRepository tasks; private final ProjectQueryService projects; - /** - * Creates the dashboard query service. - * - * @param tasks Task persistence boundary - * @param projects authorized Project query boundary - */ - public TaskDashboardService(TaskRepository tasks, ProjectQueryService projects) { - this.tasks = tasks; - this.projects = projects; - } - /** * Builds the role-scoped Task dashboard for one authenticated account. * diff --git a/src/main/java/com/lab/labtimesheet/feature/task/service/TaskQueryService.java b/src/main/java/com/lab/labtimesheet/feature/task/service/TaskQueryService.java index a50f814..ca01c04 100644 --- a/src/main/java/com/lab/labtimesheet/feature/task/service/TaskQueryService.java +++ b/src/main/java/com/lab/labtimesheet/feature/task/service/TaskQueryService.java @@ -2,6 +2,7 @@ package com.lab.labtimesheet.feature.task.service; import com.lab.labtimesheet.feature.task.repository.TaskRepository; import java.util.Set; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,19 +10,11 @@ import org.springframework.transaction.annotation.Transactional; * Public Task query boundary used by other features without exposing Task entities or repositories. */ @Service +@RequiredArgsConstructor public class TaskQueryService { private final TaskRepository tasks; - /** - * Creates the cross-feature Task query service. - * - * @param tasks Task persistence boundary - */ - public TaskQueryService(TaskRepository tasks) { - this.tasks = tasks; - } - /** * Counts current Tasks assigned outside the supplied active membership set. * diff --git a/src/main/java/com/lab/labtimesheet/feature/task/service/TaskService.java b/src/main/java/com/lab/labtimesheet/feature/task/service/TaskService.java index 302ef27..1f93653 100644 --- a/src/main/java/com/lab/labtimesheet/feature/task/service/TaskService.java +++ b/src/main/java/com/lab/labtimesheet/feature/task/service/TaskService.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,6 +43,7 @@ import org.springframework.transaction.annotation.Transactional; * to a non-disclosing Task 404, while authenticated business-rule failures use Task validation. */ @Service +@RequiredArgsConstructor public class TaskService { private final TaskRepository tasks; @@ -51,31 +53,6 @@ public class TaskService { private final CalendarApplicationService calendar; private final Clock clock; - /** - * Creates the Task application service and its feature boundaries. - * - * @param tasks Task persistence boundary - * @param comments append-only comment persistence boundary - * @param projects authorized Project read boundary - * @param projectMutations Project-first locking mutation boundary - * @param calendar authoritative global day-off query boundary - * @param clock server time source for persisted instants - */ - public TaskService( - TaskRepository tasks, - TaskCommentRepository comments, - ProjectQueryService projects, - ProjectService projectMutations, - CalendarApplicationService calendar, - Clock clock) { - this.tasks = tasks; - this.comments = comments; - this.projects = projects; - this.projectMutations = projectMutations; - this.calendar = calendar; - this.clock = clock; - } - /** * Creates a TODO Task in a PLANNED or ACTIVE Project. *