From 82ad8202fd31f77db8c3932a902dba07cee70894 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:34:11 +0700 Subject: [PATCH 1/4] refactor(attendance): target lombok boilerplate --- .../unit/lombok-attendance-boilerplate.md | 105 ++++++++++++++++++ .../controller/AttendanceController.java | 9 +- .../controller/CalendarController.java | 12 +- .../model/entity/AttendancePolicyEntity.java | 8 +- .../model/entity/AttendanceRecordEntity.java | 8 +- .../entity/GlobalCalendarEventEntity.java | 8 +- .../model/entity/LeaveRequestDayEntity.java | 8 +- .../model/entity/LeaveRequestDayId.java | 8 +- .../model/entity/LeaveRequestEntity.java | 8 +- .../service/AttendanceApplicationService.java | 22 +--- .../service/AttendanceCurrentUserService.java | 7 +- .../attendance/service/AttendanceService.java | 5 +- .../service/CalendarApplicationService.java | 12 +- .../AttendanceLombokBoilerplateTest.java | 95 ++++++++++++++++ 14 files changed, 237 insertions(+), 78 deletions(-) create mode 100644 docs/tests/unit/lombok-attendance-boilerplate.md create mode 100644 src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java diff --git a/docs/tests/unit/lombok-attendance-boilerplate.md b/docs/tests/unit/lombok-attendance-boilerplate.md new file mode 100644 index 0000000..ee1e0dd --- /dev/null +++ b/docs/tests/unit/lombok-attendance-boilerplate.md @@ -0,0 +1,105 @@ +# Test Evidence: Attendance targeted Lombok boilerplate retrofit + +- **Test type:** Unit source-contract audit +- **Requirement IDs:** `ATT-001`–`ATT-012`, `CAL-001`, `CAL-006`–`CAL-009` +- **Scenario IDs:** `AC-ATT-001`–`AC-ATT-005`, `AC-CAL-003`, `AC-CAL-004` +- **Test class/method:** `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest` +- **Implementation commit:** `pending` + +## Protected behavior + +Attendance uses the installed Lombok processor only for mechanical constructors while preserving package-level +Spring injection access, protected JPA construction, immutable records, domain constructors and mutations, raw punch +and attached-policy history rules, composite-key identity, and existing public API names. + +## Test method + +The source-contract test inspects only `feature.attendance` production Java. It enumerates the exact injection-only +components and JPA/stateless no-argument constructors eligible for Lombok, rejects retained handwritten equivalents +and blanket `@Data`, and asserts that records and business-significant methods remain explicit. + +## Hand-derived expected result + +Five injection-only components use package-scoped `@RequiredArgsConstructor`; six JPA/embeddable types use protected +`@NoArgsConstructor`; the stateless domain service uses a package-scoped `@NoArgsConstructor`. No record is replaced, +and no validated constructor, state mutation, identity method, or stable domain-style accessor is generated away. + +## RED + +**Command** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="$JAVA_HOME/bin:$PATH" +./mvnw -Dtest=AttendanceLombokBoilerplateTest test +``` + +**Observed result** + +```text +Tests run: 3, Failures: 2, Errors: 0, Skipped: 0 +The injection-component assertion first failed on AttendanceController because the required package-scoped +@RequiredArgsConstructor and Lombok imports were absent. The JPA/stateless assertion first failed on +AttendancePolicyEntity because the protected @NoArgsConstructor and Lombok imports were absent. The record and +business-method retention guard passed. +BUILD FAILURE +Process exited 1. +``` + +## GREEN + +**Command** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="$JAVA_HOME/bin:$PATH" +./mvnw -Dtest=AttendanceLombokBoilerplateTest test +``` + +**Observed result** + +```text +Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +Process exited 0. +``` + +## Affected suite + +**Command and result** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="$JAVA_HOME/bin:$PATH" +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +./mvnw -Dtest='*Attendance*Test' test +Tests run: 38, Failures: 0, Errors: 0, Skipped: 0 +PostgreSQL 18.4 started and Flyway applied V1 for the persistence and concurrency contexts. +BUILD SUCCESS +Process exited 0. +``` + +## External-test boundaries + +The source audit does not replace compilation, reflection/JPA bootstrapping, MVC property access, PostgreSQL +persistence, or Javadoc/doclint. Those checks are required as affected verification after the source contract turns +GREEN. No application behavior or public API is intentionally changed by this retrofit. + +Additional verification on the same source tree: + +```text +./mvnw -DskipTests compile +BUILD SUCCESS + +./mvnw -Dtest=AttendanceLombokBoilerplateTest,AttendanceLayerStructureTest,AttendancePolicyTest,AttendanceServiceTest,AttendanceApplicationServiceTest,AttendanceControllerTest test +Tests run: 27, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS + +./mvnw -Dtest=AttendancePersistenceIntegrationTest,AttendanceConcurrencyIntegrationTest test +Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 +PostgreSQL 18.4; BUILD SUCCESS + +javadoc -quiet -Xdoclint:all -d target/attendance-javadocs -classpath "target/classes:" -sourcepath src/main/java -subpackages com.lab.labtimesheet.feature.attendance +Process exited 0. The source frontend reported seven generated-constructor missing-comment warnings because it does not +expand Lombok constructors; repository policy exempts generated trivial constructors from duplicate Javadoc. +``` diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/controller/AttendanceController.java b/src/main/java/com/lab/labtimesheet/feature/attendance/controller/AttendanceController.java index aed3970..7e08e61 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/controller/AttendanceController.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/controller/AttendanceController.java @@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationServ import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserService; import java.security.Principal; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Controller; @@ -22,17 +24,12 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; */ @Controller @RequestMapping("/attendance") +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class AttendanceController { private final AttendanceApplicationService attendance; private final AttendanceCurrentUserService currentUsers; - AttendanceController( - AttendanceApplicationService attendance, AttendanceCurrentUserService currentUsers) { - this.attendance = attendance; - this.currentUsers = currentUsers; - } - /** * Renders the authenticated Intern's inclusive attendance history, defaulting to the current month. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/controller/CalendarController.java b/src/main/java/com/lab/labtimesheet/feature/attendance/controller/CalendarController.java index 6babad1..d43a453 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/controller/CalendarController.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/controller/CalendarController.java @@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserServ import com.lab.labtimesheet.feature.attendance.service.CalendarApplicationService; import java.security.Principal; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Controller; @@ -23,21 +25,13 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; */ @Controller @RequestMapping("/attendance/calendar") +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class CalendarController { private final CalendarApplicationService calendar; private final AttendanceApplicationService attendance; private final AttendanceCurrentUserService currentUsers; - CalendarController( - CalendarApplicationService calendar, - AttendanceApplicationService attendance, - AttendanceCurrentUserService currentUsers) { - this.calendar = calendar; - this.attendance = attendance; - this.currentUsers = currentUsers; - } - /** * Renders the next year of locally stored calendar events for an authenticated Admin. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendancePolicyEntity.java b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendancePolicyEntity.java index 59e98b9..e1cad6b 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendancePolicyEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendancePolicyEntity.java @@ -19,12 +19,15 @@ import java.time.LocalTime; import java.time.ZoneId; import java.util.Set; import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA mapping of an immutable-on-effective attendance policy version and its configured workdays. */ @Entity @Table(name = "attendance_policy_versions") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AttendancePolicyEntity { @Id @@ -65,11 +68,6 @@ public class AttendancePolicyEntity { @Version private long version; - /** - * Required by JPA; application code resolves existing effective-dated versions instead of constructing them here. - */ - protected AttendancePolicyEntity() {} - /** * Converts the persisted version to the immutable policy used for historical boundary calculations. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendanceRecordEntity.java b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendanceRecordEntity.java index 73302d9..22253cc 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendanceRecordEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/AttendanceRecordEntity.java @@ -13,12 +13,15 @@ import jakarta.persistence.Table; import jakarta.persistence.Version; import java.time.Instant; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA persistence model for one Intern/work-date punch row with its permanently attached policy version. */ @Entity @Table(name = "attendance_records") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AttendanceRecordEntity { @Id @@ -44,11 +47,6 @@ public class AttendanceRecordEntity { @Version private long version; - /** - * Required by JPA. - */ - protected AttendanceRecordEntity() {} - /** * Creates a new persistence row from server-authoritative raw punch values. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/GlobalCalendarEventEntity.java b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/GlobalCalendarEventEntity.java index c70abd6..27da114 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/GlobalCalendarEventEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/GlobalCalendarEventEntity.java @@ -10,12 +10,15 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.Version; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA model for the locally authoritative global calendar decision. */ @Entity @Table(name = "global_calendar_events") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class GlobalCalendarEventEntity { @Id @@ -43,11 +46,6 @@ public class GlobalCalendarEventEntity { @Version private long version; - /** - * Required by JPA. - */ - protected GlobalCalendarEventEntity() {} - /** * Creates a custom calendar event attributed to the Admin actor. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayEntity.java b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayEntity.java index 94cdf04..5215c59 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayEntity.java @@ -9,12 +9,15 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.MapsId; import jakarta.persistence.Table; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * JPA mapping of an immutable leave-day allocation whose exact date, policy, and quota snapshot remain historical. */ @Entity @Table(name = "leave_request_days") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class LeaveRequestDayEntity { @EmbeddedId @@ -35,11 +38,6 @@ public class LeaveRequestDayEntity { @Column(name = "monthly_quota_snapshot", nullable = false) private int monthlyQuotaSnapshot; - /** - * Required by JPA. - */ - protected LeaveRequestDayEntity() {} - LeaveRequestDayEntity( LeaveRequestEntity request, LocalDate leaveDate, diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayId.java b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayId.java index a4d59dc..20bc1fe 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayId.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestDayId.java @@ -5,11 +5,14 @@ import jakarta.persistence.Embeddable; import java.io.Serializable; import java.time.LocalDate; import java.util.Objects; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * Composite identifier of one frozen quota-consuming date within a leave request. */ @Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class LeaveRequestDayId implements Serializable { /** Parent request identity used by the composite primary key. */ @@ -20,11 +23,6 @@ public class LeaveRequestDayId implements Serializable { @Column(name = "leave_date", nullable = false) private LocalDate leaveDate; - /** - * Required by JPA. - */ - protected LeaveRequestDayId() {} - /** * Creates the identity for an already-persisted request and its exact allocated date. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestEntity.java b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestEntity.java index e40e4ed..0209325 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestEntity.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/model/entity/LeaveRequestEntity.java @@ -9,12 +9,15 @@ import jakarta.persistence.Table; import jakarta.persistence.Version; import java.time.Instant; import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; /** * Minimal Attendance-owned JPA mapping of leave request state used when evaluating frozen leave-day allocations. */ @Entity @Table(name = "leave_requests") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class LeaveRequestEntity { @Id @@ -51,11 +54,6 @@ public class LeaveRequestEntity { @Version private long version; - /** - * Required by JPA. - */ - protected LeaveRequestEntity() {} - LeaveRequestEntity( long internUserId, LocalDate startDate, diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceApplicationService.java b/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceApplicationService.java index 0882159..ce38e10 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceApplicationService.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceApplicationService.java @@ -20,9 +20,11 @@ import java.time.Instant; import java.time.LocalDate; import java.util.List; import java.util.Optional; -import org.springframework.security.access.AccessDeniedException; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional; * Account eligibility is obtained only through {@link AccountService}; raw rows retain their attached policy. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class AttendanceApplicationService { private final Clock clock; @@ -41,23 +44,6 @@ public class AttendanceApplicationService { private final CalendarApplicationService calendar; private final AttendanceService attendance; - AttendanceApplicationService( - Clock clock, - AttendancePolicyRepository policyEntities, - AttendanceRecordRepository recordEntities, - AttendanceQueryRepository queries, - AccountService accounts, - CalendarApplicationService calendar, - AttendanceService attendance) { - this.clock = clock; - this.policyEntities = policyEntities; - this.recordEntities = recordEntities; - this.queries = queries; - this.accounts = accounts; - this.calendar = calendar; - this.attendance = attendance; - } - /** * Records the sole server-time check-in for the effective policy-local date. * Eligibility, workday, calendar, and exact frozen leave allocation are evaluated in the transaction; diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceCurrentUserService.java b/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceCurrentUserService.java index 1572f94..a3063e1 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceCurrentUserService.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceCurrentUserService.java @@ -6,6 +6,8 @@ import com.lab.labtimesheet.feature.account.service.AccountService; import com.lab.labtimesheet.feature.attendance.model.AttendanceActor; import com.lab.labtimesheet.feature.attendance.model.AttendanceRole; import java.security.Principal; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; @@ -13,14 +15,11 @@ import org.springframework.stereotype.Service; * Converts Spring Security principals into active Attendance authorization contexts through AccountService DTOs. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class AttendanceCurrentUserService { private final AccountService accounts; - AttendanceCurrentUserService(AccountService accounts) { - this.accounts = accounts; - } - /** * Resolves the authenticated email through the Account feature and rejects missing or inactive identities. * diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceService.java b/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceService.java index cca61f6..77a27cc 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceService.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/service/AttendanceService.java @@ -8,16 +8,17 @@ import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord; import java.time.Instant; import java.time.LocalDate; import java.util.Optional; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.springframework.stereotype.Service; /** * Pure attendance punch rules over immutable policy, date context, and raw record values. */ @Service +@NoArgsConstructor(access = AccessLevel.PACKAGE) public final class AttendanceService { - AttendanceService() {} - /** * Creates the sole raw check-in for an eligible Intern/date using the supplied server instant. * Equality at the grace boundary is accepted; violation classification remains attached-policy based. diff --git a/src/main/java/com/lab/labtimesheet/feature/attendance/service/CalendarApplicationService.java b/src/main/java/com/lab/labtimesheet/feature/attendance/service/CalendarApplicationService.java index 72953b1..0183b9f 100644 --- a/src/main/java/com/lab/labtimesheet/feature/attendance/service/CalendarApplicationService.java +++ b/src/main/java/com/lab/labtimesheet/feature/attendance/service/CalendarApplicationService.java @@ -12,6 +12,8 @@ import com.lab.labtimesheet.feature.attendance.repository.GlobalCalendarEventRep import java.time.Clock; import java.time.LocalDate; import java.util.List; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,21 +22,13 @@ import org.springframework.transaction.annotation.Transactional; * Transactional boundary for the locally authoritative global calendar and its cross-feature day-off decision. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class CalendarApplicationService { private final Clock clock; private final AttendancePolicyRepository policies; private final GlobalCalendarEventRepository events; - CalendarApplicationService( - Clock clock, - AttendancePolicyRepository policies, - GlobalCalendarEventRepository events) { - this.clock = clock; - this.policies = policies; - this.events = events; - } - /** * Creates an Admin-authored custom event on a non-past policy-local date. * diff --git a/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java b/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java new file mode 100644 index 0000000..fd736eb --- /dev/null +++ b/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java @@ -0,0 +1,95 @@ +package com.lab.labtimesheet.feature.attendance; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; + +class AttendanceLombokBoilerplateTest { + + private static final Path SOURCE_ROOT = + Path.of("src/main/java/com/lab/labtimesheet/feature/attendance"); + + @Test + void injectionOnlyComponentsUsePackageScopedRequiredArgsConstructors() throws IOException { + for (String relativePath : List.of( + "controller/AttendanceController.java", + "controller/CalendarController.java", + "service/AttendanceApplicationService.java", + "service/AttendanceCurrentUserService.java", + "service/CalendarApplicationService.java")) { + String source = source(relativePath); + String typeName = typeName(relativePath); + + assertThat(source) + .as(relativePath) + .contains( + "import lombok.AccessLevel;", + "import lombok.RequiredArgsConstructor;", + "@RequiredArgsConstructor(access = AccessLevel.PACKAGE)") + .doesNotContain(typeName + "("); + } + } + + @Test + void jpaTypesAndStatelessServiceUseTargetedNoArgsConstructors() throws IOException { + for (String relativePath : List.of( + "model/entity/AttendancePolicyEntity.java", + "model/entity/AttendanceRecordEntity.java", + "model/entity/GlobalCalendarEventEntity.java", + "model/entity/LeaveRequestDayEntity.java", + "model/entity/LeaveRequestDayId.java", + "model/entity/LeaveRequestEntity.java")) { + String source = source(relativePath); + String typeName = typeName(relativePath); + + assertThat(source) + .as(relativePath) + .contains( + "import lombok.AccessLevel;", + "import lombok.NoArgsConstructor;", + "@NoArgsConstructor(access = AccessLevel.PROTECTED)") + .doesNotContain("protected " + typeName + "()"); + } + + assertThat(source("service/AttendanceService.java")) + .contains( + "import lombok.AccessLevel;", + "import lombok.NoArgsConstructor;", + "@NoArgsConstructor(access = AccessLevel.PACKAGE)") + .doesNotContain("AttendanceService()"); + } + + @Test + void auditRetainsRecordsAndExplicitBusinessMethodsWithoutBlanketData() throws IOException { + try (var sources = Files.walk(SOURCE_ROOT)) { + for (Path sourcePath : sources.filter(path -> path.toString().endsWith(".java")).toList()) { + assertThat(Files.readString(sourcePath)).as(sourcePath.toString()).doesNotContain("@Data"); + } + } + + assertThat(source("model/AttendancePolicy.java")).contains("public record AttendancePolicy("); + assertThat(source("model/AttendanceRecord.java")) + .contains("public record AttendanceRecord(", "public AttendanceRecord checkOut(Instant at)"); + assertThat(source("model/dto/AttendanceHistoryItem.java")) + .contains("public record AttendanceHistoryItem("); + assertThat(source("model/entity/GlobalCalendarEventEntity.java")) + .contains("public void update(", "public LocalDate calendarDate()", "public long version()"); + assertThat(source("model/entity/AttendanceRecordEntity.java")) + .contains("public void setCheckOutAt(", "public LocalDate workDate()"); + assertThat(source("model/entity/LeaveRequestDayId.java")) + .contains("public boolean equals(", "public int hashCode()"); + } + + private static String source(String relativePath) throws IOException { + return Files.readString(SOURCE_ROOT.resolve(relativePath)); + } + + private static String typeName(String relativePath) { + String fileName = Path.of(relativePath).getFileName().toString(); + return fileName.substring(0, fileName.length() - ".java".length()); + } +} From abc5208d67f366918c55c8de8fc4f2be6f89c696 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:34:34 +0700 Subject: [PATCH 2/4] docs(attendance): finalize lombok evidence --- docs/tests/unit/lombok-attendance-boilerplate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tests/unit/lombok-attendance-boilerplate.md b/docs/tests/unit/lombok-attendance-boilerplate.md index ee1e0dd..77428f3 100644 --- a/docs/tests/unit/lombok-attendance-boilerplate.md +++ b/docs/tests/unit/lombok-attendance-boilerplate.md @@ -4,7 +4,7 @@ - **Requirement IDs:** `ATT-001`–`ATT-012`, `CAL-001`, `CAL-006`–`CAL-009` - **Scenario IDs:** `AC-ATT-001`–`AC-ATT-005`, `AC-CAL-003`, `AC-CAL-004` - **Test class/method:** `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest` -- **Implementation commit:** `pending` +- **Implementation commit:** `82ad8202fd31f77db8c3932a902dba07cee70894` ## Protected behavior From 9023062477a0f3a7762294c5d3b67ef1494a5674 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:45:08 +0700 Subject: [PATCH 3/4] test(attendance): verify compiled lombok contracts --- .../unit/lombok-attendance-boilerplate.md | 46 ++- .../AttendanceLombokBoilerplateTest.java | 326 ++++++++++++++---- 2 files changed, 282 insertions(+), 90 deletions(-) diff --git a/docs/tests/unit/lombok-attendance-boilerplate.md b/docs/tests/unit/lombok-attendance-boilerplate.md index 77428f3..4a4748e 100644 --- a/docs/tests/unit/lombok-attendance-boilerplate.md +++ b/docs/tests/unit/lombok-attendance-boilerplate.md @@ -1,28 +1,35 @@ # Test Evidence: Attendance targeted Lombok boilerplate retrofit -- **Test type:** Unit source-contract audit +- **Test type:** Unit compiled-contract audit - **Requirement IDs:** `ATT-001`–`ATT-012`, `CAL-001`, `CAL-006`–`CAL-009` - **Scenario IDs:** `AC-ATT-001`–`AC-ATT-005`, `AC-CAL-003`, `AC-CAL-004` -- **Test class/method:** `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest` +- **Test class/method:** + `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#generatedConstructorsPreserveParameterListsAndVisibility`, + `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#immutableModelsRemainRecordsWithTheirComponentContracts`, + `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods` - **Implementation commit:** `82ad8202fd31f77db8c3932a902dba07cee70894` ## Protected behavior -Attendance uses the installed Lombok processor only for mechanical constructors while preserving package-level -Spring injection access, protected JPA construction, immutable records, domain constructors and mutations, raw punch -and attached-policy history rules, composite-key identity, and existing public API names. +Attendance uses the installed Lombok processor only for mechanical constructors while preserving the compiled API: +package-level Spring injection, protected JPA construction, immutable record components, domain constructors and +mutations, raw punch and attached-policy history rules, composite-key identity, and existing public method names. ## Test method -The source-contract test inspects only `feature.attendance` production Java. It enumerates the exact injection-only -components and JPA/stateless no-argument constructors eligible for Lombok, rejects retained handwritten equivalents -and blanket `@Data`, and asserts that records and business-significant methods remain explicit. +Reflection inspects compiled `feature.attendance` classes rather than source spelling. It verifies every constructor's +parameter order and modifier, every immutable model's record components, and the exact public/protected declared method +surface of each Attendance entity. The entity surface prevents generated bean getters/setters or entity +`equals`/`hashCode`/`toString` widening while explicitly retaining `AttendanceRecordEntity#setCheckOutAt` and the +`LeaveRequestDayId` identity methods. ## Hand-derived expected result -Five injection-only components use package-scoped `@RequiredArgsConstructor`; six JPA/embeddable types use protected -`@NoArgsConstructor`; the stateless domain service uses a package-scoped `@NoArgsConstructor`. No record is replaced, -and no validated constructor, state mutation, identity method, or stable domain-style accessor is generated away. +Five injection-only components expose only their package-scoped dependency constructors. Six JPA/embeddable types +retain protected no-argument construction alongside their intentional domain constructors, and the stateless domain +service remains package-scoped. Seven immutable models remain records with the same component order and types. Entity +method surfaces contain only intentional domain conversion/access/mutation methods; only the composite key owns +`equals` and `hashCode`, and no Attendance entity declares `toString`. ## RED @@ -38,10 +45,10 @@ export PATH="$JAVA_HOME/bin:$PATH" ```text Tests run: 3, Failures: 2, Errors: 0, Skipped: 0 -The injection-component assertion first failed on AttendanceController because the required package-scoped -@RequiredArgsConstructor and Lombok imports were absent. The JPA/stateless assertion first failed on -AttendancePolicyEntity because the protected @NoArgsConstructor and Lombok imports were absent. The record and -business-method retention guard passed. +The initial source audit first failed on AttendanceController because its mechanical dependency constructor remained, +and on AttendancePolicyEntity because its mechanical protected JPA constructor remained. The immutable-record and +business-method retention guard passed. After this RED established the retrofit gap, the permanent regression was +replaced with compiled reflection/API checks so formatting or annotation spelling cannot affect the result. BUILD FAILURE Process exited 1. ``` @@ -81,9 +88,9 @@ Process exited 0. ## External-test boundaries -The source audit does not replace compilation, reflection/JPA bootstrapping, MVC property access, PostgreSQL -persistence, or Javadoc/doclint. Those checks are required as affected verification after the source contract turns -GREEN. No application behavior or public API is intentionally changed by this retrofit. +The reflection audit does not replace Spring/JPA bootstrapping, MVC property access, PostgreSQL persistence, or +Javadoc/doclint. Those checks remain affected verification. No application behavior or public API is intentionally +changed by this retrofit. Additional verification on the same source tree: @@ -99,7 +106,8 @@ BUILD SUCCESS Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 PostgreSQL 18.4; BUILD SUCCESS -javadoc -quiet -Xdoclint:all -d target/attendance-javadocs -classpath "target/classes:" -sourcepath src/main/java -subpackages com.lab.labtimesheet.feature.attendance +./mvnw -q -DskipTests compile dependency:build-classpath -Dmdep.outputFile=target/attendance-javadoc-classpath.txt +javadoc -quiet -Xdoclint:all -d target/attendance-javadocs -classpath "target/classes:$(tr -d '\n' < target/attendance-javadoc-classpath.txt)" -sourcepath src/main/java -subpackages com.lab.labtimesheet.feature.attendance Process exited 0. The source frontend reported seven generated-constructor missing-comment warnings because it does not expand Lombok constructors; repository policy exempts generated trivial constructors from duplicate Javadoc. ``` diff --git a/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java b/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java index fd736eb..256b01f 100644 --- a/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java +++ b/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java @@ -2,94 +2,278 @@ package com.lab.labtimesheet.feature.attendance; import static org.assertj.core.api.Assertions.assertThat; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import com.lab.labtimesheet.feature.account.service.AccountService; +import com.lab.labtimesheet.feature.attendance.controller.AttendanceController; +import com.lab.labtimesheet.feature.attendance.controller.CalendarController; +import com.lab.labtimesheet.feature.attendance.model.AttendanceActor; +import com.lab.labtimesheet.feature.attendance.model.AttendanceDayContext; +import com.lab.labtimesheet.feature.attendance.model.AttendancePolicy; +import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord; +import com.lab.labtimesheet.feature.attendance.model.AttendanceRole; +import com.lab.labtimesheet.feature.attendance.model.AttendanceViolations; +import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceHistoryItem; +import com.lab.labtimesheet.feature.attendance.model.dto.GlobalCalendarEvent; +import com.lab.labtimesheet.feature.attendance.model.entity.AttendancePolicyEntity; +import com.lab.labtimesheet.feature.attendance.model.entity.AttendanceRecordEntity; +import com.lab.labtimesheet.feature.attendance.model.entity.GlobalCalendarEventEntity; +import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestDayEntity; +import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestDayId; +import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestEntity; +import com.lab.labtimesheet.feature.attendance.repository.AttendancePolicyRepository; +import com.lab.labtimesheet.feature.attendance.repository.AttendanceQueryRepository; +import com.lab.labtimesheet.feature.attendance.repository.AttendanceRecordRepository; +import com.lab.labtimesheet.feature.attendance.repository.GlobalCalendarEventRepository; +import com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationService; +import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserService; +import com.lab.labtimesheet.feature.attendance.service.AttendanceService; +import com.lab.labtimesheet.feature.attendance.service.CalendarApplicationService; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.Arrays; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; class AttendanceLombokBoilerplateTest { - private static final Path SOURCE_ROOT = - Path.of("src/main/java/com/lab/labtimesheet/feature/attendance"); + private static final int PACKAGE_PRIVATE = 0; @Test - void injectionOnlyComponentsUsePackageScopedRequiredArgsConstructors() throws IOException { - for (String relativePath : List.of( - "controller/AttendanceController.java", - "controller/CalendarController.java", - "service/AttendanceApplicationService.java", - "service/AttendanceCurrentUserService.java", - "service/CalendarApplicationService.java")) { - String source = source(relativePath); - String typeName = typeName(relativePath); + void generatedConstructorsPreserveParameterListsAndVisibility() { + assertConstructors( + AttendanceController.class, + constructor( + PACKAGE_PRIVATE, + AttendanceApplicationService.class, + AttendanceCurrentUserService.class)); + assertConstructors( + CalendarController.class, + constructor( + PACKAGE_PRIVATE, + CalendarApplicationService.class, + AttendanceApplicationService.class, + AttendanceCurrentUserService.class)); + assertConstructors( + AttendanceApplicationService.class, + constructor( + PACKAGE_PRIVATE, + Clock.class, + AttendancePolicyRepository.class, + AttendanceRecordRepository.class, + AttendanceQueryRepository.class, + AccountService.class, + CalendarApplicationService.class, + AttendanceService.class)); + assertConstructors( + AttendanceCurrentUserService.class, + constructor(PACKAGE_PRIVATE, AccountService.class)); + assertConstructors( + CalendarApplicationService.class, + constructor( + PACKAGE_PRIVATE, + Clock.class, + AttendancePolicyRepository.class, + GlobalCalendarEventRepository.class)); + assertConstructors(AttendanceService.class, constructor(PACKAGE_PRIVATE)); - assertThat(source) - .as(relativePath) - .contains( - "import lombok.AccessLevel;", - "import lombok.RequiredArgsConstructor;", - "@RequiredArgsConstructor(access = AccessLevel.PACKAGE)") - .doesNotContain(typeName + "("); - } + assertConstructors(AttendancePolicyEntity.class, constructor(Modifier.PROTECTED)); + assertConstructors( + AttendanceRecordEntity.class, + constructor(Modifier.PROTECTED), + constructor( + Modifier.PUBLIC, + long.class, + LocalDate.class, + AttendancePolicyEntity.class, + Instant.class, + Instant.class)); + assertConstructors( + GlobalCalendarEventEntity.class, + constructor(Modifier.PROTECTED), + constructor( + Modifier.PUBLIC, + LocalDate.class, + String.class, + boolean.class, + long.class)); + assertConstructors( + LeaveRequestDayEntity.class, + constructor(Modifier.PROTECTED), + constructor( + PACKAGE_PRIVATE, + LeaveRequestEntity.class, + LocalDate.class, + AttendancePolicyEntity.class, + int.class)); + assertConstructors( + LeaveRequestDayId.class, + constructor(Modifier.PROTECTED), + constructor(Modifier.PUBLIC, long.class, LocalDate.class)); + assertConstructors( + LeaveRequestEntity.class, + constructor(Modifier.PROTECTED), + constructor( + PACKAGE_PRIVATE, + long.class, + LocalDate.class, + LocalDate.class, + String.class, + Instant.class, + Instant.class, + long.class, + Instant.class)); } @Test - void jpaTypesAndStatelessServiceUseTargetedNoArgsConstructors() throws IOException { - for (String relativePath : List.of( - "model/entity/AttendancePolicyEntity.java", - "model/entity/AttendanceRecordEntity.java", - "model/entity/GlobalCalendarEventEntity.java", - "model/entity/LeaveRequestDayEntity.java", - "model/entity/LeaveRequestDayId.java", - "model/entity/LeaveRequestEntity.java")) { - String source = source(relativePath); - String typeName = typeName(relativePath); - - assertThat(source) - .as(relativePath) - .contains( - "import lombok.AccessLevel;", - "import lombok.NoArgsConstructor;", - "@NoArgsConstructor(access = AccessLevel.PROTECTED)") - .doesNotContain("protected " + typeName + "()"); - } - - assertThat(source("service/AttendanceService.java")) - .contains( - "import lombok.AccessLevel;", - "import lombok.NoArgsConstructor;", - "@NoArgsConstructor(access = AccessLevel.PACKAGE)") - .doesNotContain("AttendanceService()"); + void immutableModelsRemainRecordsWithTheirComponentContracts() { + assertRecordComponents( + AttendanceActor.class, + component("userId", long.class), + component("role", AttendanceRole.class)); + assertRecordComponents( + AttendanceDayContext.class, + component("activeIntern", boolean.class), + component("globalDayOff", boolean.class), + component("approvedLeave", boolean.class)); + assertRecordComponents( + AttendancePolicy.class, + component("id", long.class), + component("effectiveFrom", LocalDate.class), + component("zoneId", ZoneId.class), + component("scheduledStart", LocalTime.class), + component("scheduledEnd", LocalTime.class), + component("checkInGraceMinutes", int.class), + component("checkoutGraceMinutes", int.class), + component("monthlyLeaveQuota", int.class), + component("violationPenalty", BigDecimal.class), + component("workdays", Set.class)); + assertRecordComponents( + AttendanceRecord.class, + component("internId", long.class), + component("workDate", LocalDate.class), + component("policy", AttendancePolicy.class), + component("checkInAt", Instant.class), + component("checkOutAt", Instant.class)); + assertRecordComponents( + AttendanceViolations.class, + component("late", boolean.class), + component("earlyDeparture", boolean.class), + component("missingCheckout", boolean.class)); + assertRecordComponents( + AttendanceHistoryItem.class, + component("workDate", LocalDate.class), + component("checkInAt", Instant.class), + component("checkOutAt", Instant.class), + component("policy", AttendancePolicy.class), + component("violations", AttendanceViolations.class)); + assertRecordComponents( + GlobalCalendarEvent.class, + component("id", long.class), + component("date", LocalDate.class), + component("name", String.class), + component("dayOff", boolean.class), + component("version", long.class)); } @Test - void auditRetainsRecordsAndExplicitBusinessMethodsWithoutBlanketData() throws IOException { - try (var sources = Files.walk(SOURCE_ROOT)) { - for (Path sourcePath : sources.filter(path -> path.toString().endsWith(".java")).toList()) { - assertThat(Files.readString(sourcePath)).as(sourcePath.toString()).doesNotContain("@Data"); - } + void entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods() { + assertMethodSurface( + AttendancePolicyEntity.class, + method(Modifier.PUBLIC, "toDomain", AttendancePolicy.class)); + assertMethodSurface( + AttendanceRecordEntity.class, + method(Modifier.PUBLIC, "toDomain", AttendanceRecord.class), + method(Modifier.PUBLIC, "setCheckOutAt", void.class, Instant.class), + method(Modifier.PUBLIC, "workDate", LocalDate.class)); + assertMethodSurface( + GlobalCalendarEventEntity.class, + method( + Modifier.PUBLIC, + "update", + void.class, + LocalDate.class, + String.class, + boolean.class, + long.class), + method(Modifier.PUBLIC, "toDomain", GlobalCalendarEvent.class), + method(Modifier.PUBLIC, "calendarDate", LocalDate.class), + method(Modifier.PUBLIC, "version", long.class)); + assertMethodSurface(LeaveRequestDayEntity.class); + assertMethodSurface( + LeaveRequestDayId.class, + method(Modifier.PUBLIC, "equals", boolean.class, Object.class), + method(Modifier.PUBLIC, "hashCode", int.class)); + assertMethodSurface(LeaveRequestEntity.class); + } + + private static void assertConstructors( + Class type, ConstructorContract... expectedConstructors) { + List actual = Arrays.stream(type.getDeclaredConstructors()) + .map(ConstructorContract::from) + .toList(); + + assertThat(actual).as(type.getName()).containsExactlyInAnyOrder(expectedConstructors); + } + + private static void assertRecordComponents( + Class type, RecordComponentContract... expectedComponents) { + assertThat(type.isRecord()).as(type.getName()).isTrue(); + assertThat(Arrays.stream(type.getRecordComponents()) + .map(component -> new RecordComponentContract(component.getName(), component.getType())) + .toList()) + .as(type.getName()) + .containsExactly(expectedComponents); + } + + private static void assertMethodSurface(Class type, MethodContract... expectedMethods) { + List actual = Arrays.stream(type.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers()) + || Modifier.isProtected(method.getModifiers())) + .map(MethodContract::from) + .toList(); + + assertThat(actual).as(type.getName()).containsExactlyInAnyOrder(expectedMethods); + } + + private static ConstructorContract constructor(int modifiers, Class... parameterTypes) { + return new ConstructorContract(modifiers, List.of(parameterTypes)); + } + + private static RecordComponentContract component(String name, Class type) { + return new RecordComponentContract(name, type); + } + + private static MethodContract method( + int modifiers, String name, Class returnType, Class... parameterTypes) { + return new MethodContract(modifiers, name, returnType, List.of(parameterTypes)); + } + + private record ConstructorContract(int modifiers, List> parameterTypes) { + + private static ConstructorContract from(Constructor constructor) { + return new ConstructorContract( + constructor.getModifiers(), List.of(constructor.getParameterTypes())); } - - assertThat(source("model/AttendancePolicy.java")).contains("public record AttendancePolicy("); - assertThat(source("model/AttendanceRecord.java")) - .contains("public record AttendanceRecord(", "public AttendanceRecord checkOut(Instant at)"); - assertThat(source("model/dto/AttendanceHistoryItem.java")) - .contains("public record AttendanceHistoryItem("); - assertThat(source("model/entity/GlobalCalendarEventEntity.java")) - .contains("public void update(", "public LocalDate calendarDate()", "public long version()"); - assertThat(source("model/entity/AttendanceRecordEntity.java")) - .contains("public void setCheckOutAt(", "public LocalDate workDate()"); - assertThat(source("model/entity/LeaveRequestDayId.java")) - .contains("public boolean equals(", "public int hashCode()"); } - private static String source(String relativePath) throws IOException { - return Files.readString(SOURCE_ROOT.resolve(relativePath)); - } + private record RecordComponentContract(String name, Class type) {} - private static String typeName(String relativePath) { - String fileName = Path.of(relativePath).getFileName().toString(); - return fileName.substring(0, fileName.length() - ".java".length()); + private record MethodContract( + int modifiers, String name, Class returnType, List> parameterTypes) { + + private static MethodContract from(Method method) { + return new MethodContract( + method.getModifiers(), + method.getName(), + method.getReturnType(), + List.of(method.getParameterTypes())); + } } } From 5ddbd75f075c5da80eb032109fe14e0f2e6e868d Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:49:53 +0700 Subject: [PATCH 4/4] test(attendance): guard component method surfaces --- .../unit/lombok-attendance-boilerplate.md | 21 ++-- .../AttendanceLombokBoilerplateTest.java | 119 ++++++++++++++++++ 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/docs/tests/unit/lombok-attendance-boilerplate.md b/docs/tests/unit/lombok-attendance-boilerplate.md index 4a4748e..7a836f3 100644 --- a/docs/tests/unit/lombok-attendance-boilerplate.md +++ b/docs/tests/unit/lombok-attendance-boilerplate.md @@ -6,7 +6,8 @@ - **Test class/method:** `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#generatedConstructorsPreserveParameterListsAndVisibility`, `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#immutableModelsRemainRecordsWithTheirComponentContracts`, - `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods` + `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods`, + `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#componentsExposeOnlyIntentionalPublicAndProtectedDeclaredMethods` - **Implementation commit:** `82ad8202fd31f77db8c3932a902dba07cee70894` ## Protected behavior @@ -21,7 +22,8 @@ Reflection inspects compiled `feature.attendance` classes rather than source spe parameter order and modifier, every immutable model's record components, and the exact public/protected declared method surface of each Attendance entity. The entity surface prevents generated bean getters/setters or entity `equals`/`hashCode`/`toString` widening while explicitly retaining `AttendanceRecordEntity#setCheckOutAt` and the -`LeaveRequestDayId` identity methods. +`LeaveRequestDayId` identity methods. Exact controller and service surfaces likewise prevent Lombok from exposing +collaborator getters/setters or generated `equals`/`hashCode`/`toString` methods. ## Hand-derived expected result @@ -29,7 +31,8 @@ Five injection-only components expose only their package-scoped dependency const retain protected no-argument construction alongside their intentional domain constructors, and the stateless domain service remains package-scoped. Seven immutable models remain records with the same component order and types. Entity method surfaces contain only intentional domain conversion/access/mutation methods; only the composite key owns -`equals` and `hashCode`, and no Attendance entity declares `toString`. +`equals` and `hashCode`, and no Attendance entity declares `toString`. Both controllers and all four Attendance +services expose only their existing route or application/domain operations, never their injected collaborators. ## RED @@ -66,7 +69,7 @@ export PATH="$JAVA_HOME/bin:$PATH" **Observed result** ```text -Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 +Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS Process exited 0. ``` @@ -80,7 +83,7 @@ export JAVA_HOME=/opt/homebrew/opt/openjdk@25 export PATH="$JAVA_HOME/bin:$PATH" export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw -Dtest='*Attendance*Test' test -Tests run: 38, Failures: 0, Errors: 0, Skipped: 0 +Tests run: 39, Failures: 0, Errors: 0, Skipped: 0 PostgreSQL 18.4 started and Flyway applied V1 for the persistence and concurrency contexts. BUILD SUCCESS Process exited 0. @@ -98,14 +101,6 @@ Additional verification on the same source tree: ./mvnw -DskipTests compile BUILD SUCCESS -./mvnw -Dtest=AttendanceLombokBoilerplateTest,AttendanceLayerStructureTest,AttendancePolicyTest,AttendanceServiceTest,AttendanceApplicationServiceTest,AttendanceControllerTest test -Tests run: 27, Failures: 0, Errors: 0, Skipped: 0 -BUILD SUCCESS - -./mvnw -Dtest=AttendancePersistenceIntegrationTest,AttendanceConcurrencyIntegrationTest test -Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 -PostgreSQL 18.4; BUILD SUCCESS - ./mvnw -q -DskipTests compile dependency:build-classpath -Dmdep.outputFile=target/attendance-javadoc-classpath.txt javadoc -quiet -Xdoclint:all -d target/attendance-javadocs -classpath "target/classes:$(tr -d '\n' < target/attendance-javadoc-classpath.txt)" -sourcepath src/main/java -subpackages com.lab.labtimesheet.feature.attendance Process exited 0. The source frontend reported seven generated-constructor missing-comment warnings because it does not diff --git a/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java b/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java index 256b01f..5764e20 100644 --- a/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java +++ b/src/test/java/com/lab/labtimesheet/feature/attendance/AttendanceLombokBoilerplateTest.java @@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.attendance.model.AttendancePolicy; import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord; import com.lab.labtimesheet.feature.attendance.model.AttendanceRole; import com.lab.labtimesheet.feature.attendance.model.AttendanceViolations; +import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceCurrentState; import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceHistoryItem; import com.lab.labtimesheet.feature.attendance.model.dto.GlobalCalendarEvent; import com.lab.labtimesheet.feature.attendance.model.entity.AttendancePolicyEntity; @@ -31,6 +32,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; +import java.security.Principal; import java.time.Clock; import java.time.Instant; import java.time.LocalDate; @@ -38,8 +40,11 @@ import java.time.LocalTime; import java.time.ZoneId; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; +import org.springframework.ui.Model; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; class AttendanceLombokBoilerplateTest { @@ -213,6 +218,120 @@ class AttendanceLombokBoilerplateTest { assertMethodSurface(LeaveRequestEntity.class); } + @Test + void componentsExposeOnlyIntentionalPublicAndProtectedDeclaredMethods() { + assertMethodSurface( + AttendanceController.class, + method( + Modifier.PUBLIC, + "ownHistory", + String.class, + Principal.class, + LocalDate.class, + LocalDate.class, + Model.class), + method( + Modifier.PUBLIC, + "inspectHistory", + String.class, + Principal.class, + long.class, + LocalDate.class, + LocalDate.class, + Model.class), + method( + Modifier.PUBLIC, + "checkIn", + String.class, + Principal.class, + RedirectAttributes.class), + method( + Modifier.PUBLIC, + "checkOut", + String.class, + Principal.class, + RedirectAttributes.class)); + assertMethodSurface( + CalendarController.class, + method(Modifier.PUBLIC, "calendar", String.class, Principal.class, Model.class), + method( + Modifier.PUBLIC, + "create", + String.class, + Principal.class, + LocalDate.class, + String.class, + boolean.class, + RedirectAttributes.class), + method( + Modifier.PUBLIC, + "update", + String.class, + Principal.class, + long.class, + long.class, + LocalDate.class, + String.class, + boolean.class, + RedirectAttributes.class)); + assertMethodSurface( + AttendanceApplicationService.class, + method(Modifier.PUBLIC, "checkIn", AttendanceRecord.class, long.class), + method(Modifier.PUBLIC, "checkOut", AttendanceRecord.class, long.class), + method(Modifier.PUBLIC, "currentState", AttendanceCurrentState.class, long.class), + method( + Modifier.PUBLIC, + "history", + List.class, + AttendanceActor.class, + long.class, + LocalDate.class, + LocalDate.class), + method(Modifier.PUBLIC, "currentBusinessDate", LocalDate.class)); + assertMethodSurface( + AttendanceCurrentUserService.class, + method(Modifier.PUBLIC, "actor", AttendanceActor.class, Principal.class)); + assertMethodSurface( + AttendanceService.class, + method( + Modifier.PUBLIC, + "checkIn", + AttendanceRecord.class, + long.class, + Instant.class, + AttendancePolicy.class, + AttendanceDayContext.class, + Optional.class), + method( + Modifier.PUBLIC, + "checkOut", + AttendanceRecord.class, + Optional.class, + Instant.class)); + assertMethodSurface( + CalendarApplicationService.class, + method( + Modifier.PUBLIC, + "createManual", + GlobalCalendarEvent.class, + AttendanceActor.class, + LocalDate.class, + String.class, + boolean.class), + method( + Modifier.PUBLIC, + "updateManual", + GlobalCalendarEvent.class, + AttendanceActor.class, + long.class, + long.class, + LocalDate.class, + String.class, + boolean.class), + method(Modifier.PUBLIC, "list", List.class, LocalDate.class, LocalDate.class), + method(Modifier.PUBLIC, "isGlobalDayOff", boolean.class, LocalDate.class)); + } + private static void assertConstructors( Class type, ConstructorContract... expectedConstructors) { List actual = Arrays.stream(type.getDeclaredConstructors())