test(attendance): verify compiled lombok contracts

This commit is contained in:
sechmachine
2026-08-15 11:45:08 +07:00
parent abc5208d67
commit 9023062477
2 changed files with 282 additions and 90 deletions
@@ -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<ConstructorContract> 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<MethodContract> 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<Class<?>> 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<Class<?>> parameterTypes) {
private static MethodContract from(Method method) {
return new MethodContract(
method.getModifiers(),
method.getName(),
method.getReturnType(),
List.of(method.getParameterTypes()));
}
}
}