diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/UserSchedulerService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/UserSchedulerService.java new file mode 100644 index 00000000..1027bda3 --- /dev/null +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/UserSchedulerService.java @@ -0,0 +1,134 @@ +package DGU_AI_LAB.admin_be.domain.scheduler; + +import DGU_AI_LAB.admin_be.domain.alarm.service.AlarmService; +import DGU_AI_LAB.admin_be.domain.users.entity.User; +import DGU_AI_LAB.admin_be.domain.users.repository.UserRepository; +import DGU_AI_LAB.admin_be.global.util.MessageUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UserSchedulerService { + + private final UserRepository userRepository; + private final AlarmService alarmService; + private final MessageUtils messageUtils; + + private static final int INACTIVE_MONTHS = 3; + private static final int HARD_DELETE_YEARS = 1; + + // 매일 오전 09:00 실행 + @Scheduled(cron = "0 0 9 * * ?", zone = "Asia/Seoul") + @Transactional + public void runUserLifecycleScheduler() { + log.info("👤 [스케줄러 시작] 사용자 계정 수명주기 관리"); + LocalDateTime now = LocalDateTime.now(); + + // 1. Soft Delete 대상자 처리 (및 예고 알림) + processInactiveUsers(now); + + // 2. Hard Delete 대상자 처리 + processHardDeleteUsers(now); + + log.info("👤 [스케줄러 종료]"); + } + + /** + * 장기 미접속자 조회 및 처리 (경고 알림 OR Soft Delete) + */ + private void processInactiveUsers(LocalDateTime now) { + // 기준일: 오늘 - 3개월 + 7일 (7일 전 알림을 위해 여유 있게 조회 후 로직에서 필터링) + // 사실상 3개월 전 즈음에 활동이 멈춘 사람들을 모두 가져옴 + LocalDateTime searchThreshold = now.minusMonths(INACTIVE_MONTHS).plusDays(8); + List inactiveCandidates = userRepository.findInactiveUsers(searchThreshold); + + for (User user : inactiveCandidates) { + try { + // 이 유저의 "활동 만료일(삭제 예정일)" 계산 + // 만료일 = Max(LastLogin, LastPodExpire) + 3개월 + LocalDateTime lastActivity = user.getLastLoginAt(); + + // 쿼리에서 이미 필터링했지만, Java 단에서 정확한 D-Day 계산을 위해 다시 확인 + // Request는 Lazy Loading이므로 트랜잭션 내에서 접근 가능 + if (!user.getRequests().isEmpty()) { + LocalDateTime lastPodExpire = user.getRequests().stream() + .map(req -> req.getExpiresAt()) + .max(LocalDateTime::compareTo) + .orElse(LocalDateTime.MIN); + + if (lastPodExpire.isAfter(lastActivity)) { + lastActivity = lastPodExpire; + } + } + + LocalDateTime deleteDate = lastActivity.plusMonths(INACTIVE_MONTHS); + long daysLeft = ChronoUnit.DAYS.between(now.toLocalDate(), deleteDate.toLocalDate()); + + // 1) 예고 알림 (D-7, D-3, D-1) + if (daysLeft == 7 || daysLeft == 3 || daysLeft == 1) { + sendWarningAlert(user, daysLeft, deleteDate); + } + // 2) Soft Delete 실행 (D-Day 또는 그 이후) + else if (daysLeft <= 0) { + softDeleteUser(user); + } + + } catch (Exception e) { + log.error("유저({}) 수명주기 처리 중 오류: {}", user.getEmail(), e.getMessage()); + } + } + } + + private void sendWarningAlert(User user, long daysLeft, LocalDateTime deleteDate) { + String dateStr = deleteDate.toLocalDate().toString(); + + String subject = messageUtils.get("notification.user.delete-warning.subject", String.valueOf(daysLeft)); + String body = messageUtils.get("notification.user.delete-warning.body", + user.getName(), String.valueOf(daysLeft), dateStr); + + alarmService.sendAllAlerts(user.getName(), user.getEmail(), subject, body); + log.info("경고 알림 발송: {} ({}일 전)", user.getEmail(), daysLeft); + } + + private void softDeleteUser(User user) { + user.withdraw(); // isActive = false, deletedAt = now + + String subject = messageUtils.get("notification.user.soft-delete.subject"); + String body = messageUtils.get("notification.user.soft-delete.body", user.getName()); + + alarmService.sendAllAlerts(user.getName(), user.getEmail(), subject, body); + log.info("계정 비활성화(Soft Delete) 완료: {}", user.getEmail()); + } + + /** + * Hard Delete (개인정보 완전 삭제) + */ + private void processHardDeleteUsers(LocalDateTime now) { + LocalDateTime hardDeleteThreshold = now.minusYears(HARD_DELETE_YEARS); + List hardDeleteTargets = userRepository.findUsersForHardDelete(hardDeleteThreshold); + + for (User user : hardDeleteTargets) { + try { + Long userId = user.getUserId(); + String email = user.getEmail(); + + // DB 완전 삭제 (Cascade 설정으로 인해 연관 Request도 삭제됨) + userRepository.delete(user); + + log.info("계정 영구 삭제(Hard Delete) 완료: ID={}, Email={}", userId, email); + } catch (Exception e) { + log.error("Hard Delete 실패: {}", user.getEmail(), e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java index 594eaf62..fcf6ac85 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java @@ -1,9 +1,14 @@ package DGU_AI_LAB.admin_be.domain.users.entity; +import DGU_AI_LAB.admin_be.domain.requests.entity.Request; import DGU_AI_LAB.admin_be.global.common.BaseTimeEntity; import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + @Entity @Table(name = "users") @Getter @@ -41,6 +46,15 @@ public class User extends BaseTimeEntity { @Column(name = "is_active", nullable = false) private Boolean isActive = true; + @Column(name = "last_login_at") + private LocalDateTime lastLoginAt; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List requests = new ArrayList<>(); + @Builder public User(String email, String password, String name, String studentId, String phone, String department) { this.email = email; @@ -49,6 +63,7 @@ public User(String email, String password, String name, String studentId, String this.studentId = studentId; this.phone = phone; this.department = department; + this.lastLoginAt = LocalDateTime.now(); } // ===== 비즈니스 메서드 ===== @@ -65,4 +80,13 @@ public void updatePassword(String newEncodedPassword) { public void updatePhone(String newPhone) { this.phone = newPhone; } + + public void recordLogin() { + this.lastLoginAt = LocalDateTime.now(); + } + + public void withdraw() { + this.isActive = false; + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/repository/UserRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/repository/UserRepository.java index 00a3b858..12922488 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/repository/UserRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/repository/UserRepository.java @@ -2,11 +2,41 @@ import DGU_AI_LAB.admin_be.domain.users.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + /** + * [자동 탈퇴 대상 조회 쿼리] + * 조건: + * 1. Active 상태인 유저 + * 2. (현재 - 마지막 로그인) > 3개월 + * 3. (현재 - 가장 최근 만료된 Pod 날짜) > 3개월 (Pod 사용 기록이 없으면 로그인 날짜만 봄) + * * 주의: COALESCE를 사용하여 Pod 기록이 없으면 아주 먼 과거(1900년)로 취급해 조건 통과시킴 + */ + @Query("SELECT u FROM User u " + + "LEFT JOIN u.requests r " + + "WHERE u.isActive = true " + + "GROUP BY u " + + "HAVING " + + " (u.lastLoginAt IS NULL OR u.lastLoginAt < :thresholdDate) " + + " AND " + + " (MAX(r.expiresAt) IS NULL OR MAX(r.expiresAt) < :thresholdDate)") + List findInactiveUsers(@Param("thresholdDate") LocalDateTime thresholdDate); + + /** + * [Hard Delete 대상 조회] + * 조건: Soft Delete 된 지 1년 지난 유저 + */ + @Query("SELECT u FROM User u WHERE u.isActive = false AND u.deletedAt < :hardDeleteThreshold") + List findUsersForHardDelete(@Param("hardDeleteThreshold") LocalDateTime hardDeleteThreshold); } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserLoginService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserLoginService.java index 429b624f..987ac6b7 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserLoginService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserLoginService.java @@ -60,10 +60,20 @@ public UserTokenResponseDTO login(UserLoginRequestDTO request) { User user = userRepository.findByEmail(request.email()) .orElseThrow(() -> new UnauthorizedException(ErrorCode.INVALID_LOGIN_INFO)); + if (!user.getIsActive()) { + throw new UnauthorizedException(ErrorCode.ACCOUNT_DISABLED); + } + if (!passwordEncoder.matches(request.password(), user.getPassword())) { throw new UnauthorizedException(ErrorCode.INVALID_LOGIN_INFO); } + if (!passwordEncoder.matches(request.password(), user.getPassword())) { + throw new UnauthorizedException(ErrorCode.INVALID_LOGIN_INFO); + } + + user.recordLogin(); + String accessToken = jwtProvider.getIssueToken(user.getUserId(), true); String refreshToken = jwtProvider.getIssueToken(user.getUserId(), false); diff --git a/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java b/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java index ed96f73f..4da8cac6 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java +++ b/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java @@ -87,6 +87,7 @@ public enum ErrorCode { GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "지정된 그룹을 찾을 수 없습니다."), UID_ALLOCATION_FAILED(HttpStatus.BAD_REQUEST, "UID를 할당에 실패했습니다."), DUPLICATE_USERNAME(HttpStatus.CONFLICT, "이미 사용하고 있는 username입니다. 같은 사용자이더라도 다른 username을 입력해주세요."), + ACCOUNT_DISABLED(HttpStatus.NOT_FOUND,"비활성화된 유저입니다. 관리자에게 문의하세요."), SLACK_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "Slack 사용자를 찾을 수 없습니다."), SLACK_USER_EMAIL_NOT_MATCH(HttpStatus.NOT_FOUND, "이메일이 일치하는 Slack 사용자를 찾을 수 없습니다."), diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 438954f8..9aaaaff1 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -2,26 +2,62 @@ # Slack & Email Notification Messages # ============================================================================== -# ?? ?? (???) +# 1. ?? ?? (???) # {0}: ??? ?? notification.expired.dm=????? {0}?, ???? GPU ?? ?? ??? ???? ???? ???????. +# {0}: ??? ??, {1}: ???, {2}: ??? notification.expired.detail.subject=[DGU AI LAB] ?? ?? ?? ?? ?? -notification.expired.detail.body=?????, {0}?.\n\n?? ??? ?? ?? ?? ???? ???????.\n\n- ??: {1}\n- ??: {2}\n\n??? ??? ?????. +notification.expired.detail.body=?????, {0}?.\n\ + \n?? ??? ?? ?? ?? ???? ???????.\n\ + \n- ??: {1}\n\ + - ??: {2}\n\ + \n??? ??? ?????. + +# 2. ?? ?? (7?, 3?, 1? ?) +# Subject ???: {1} -> {0} (??? dayLabel ????) +notification.pre-expiry.subject=[DGU AI LAB] ?? ?? ?? ?? ?? ({0} ?) -# ?? ?? (7?, 3?, 1? ?) # {0}: ??? ??, {1}: ?? ??(ex. 7?), {2}: ???, {3}: ???, {4}: ??? -notification.pre-expiry.subject=[DGU AI LAB] ?? ?? ?? ?? ?? ({1} ?) -notification.pre-expiry.body=?????, {0}?.\n\n?? ?? GPU ?? ??? {1} ? ({2})? ???? ??? ?????.\n\n- ??: {3}\n- ??: {4}\n\n??? ???? ??? ? ???, ??? ???? ?? ??? ??? ????.\n??? ????? ????? ?????. +notification.pre-expiry.body=?????, {0}?.\n\ + \n?? ?? GPU ?? ??? {1} ? ({2})? ???? ??? ?????.\n\ + \n- ??: {3}\n\ + - ??: {4}\n\ + \n??? ???? ??? ? ???, ??? ???? ?? ??? ??? ????.\n\ + ??? ????? ????? ?????. -# ??? ?? +# 3. ??? ?? +# {0}: ??(Farm/Lab), {1}: ???, {2}: ???, {3}: ???? notification.admin.delete.success=?? [{0}] ??? ?? ??: {1} ({2}) notification.admin.delete.fail=? [{0}] ??? ?? ??!\n- ??: {1}\n- ??: {2}\n- ??: {3} notification.admin.new-request=? ??? ?? ?? ??! ?\n? ???: {0}\n? ??: {1}\n(??? ??? ?? ??) -# ?? ?? +# 4. ?? ?? notification.approval.subject=[DGU AI LAB] ?? ?? ?? ?? notification.approval.body=? {0}?? ??? ???????. -# ??? ?? ??? (Fallback) -notification.error.redis-fallback=\n[?? Redis ??? ?? ???] \ No newline at end of file +# 5. ??? ?? ??? (Fallback) +notification.error.redis-fallback=\n[?? Redis ??? ?? ???] + +# ============================================================================== +# User Auto-Deletion Messages +# ============================================================================== + +# 1. ?? ?? ?? (D-7, D-3, D-1) +# Subject: {0}: ?? ?? +notification.user.delete-warning.subject=[DGU AI LAB] ?? ??? ?? ?? ?? ?? ({0}? ?) + +# Body: {0}: ??, {1}: ?? ??, {2}: ?? ??? +notification.user.delete-warning.body=?????, {0}?.\n\ + \n???? ??? ?? ???(3?? ??) ? ??? ????? ?? \ + {1}? ? ({2})? ???? ??? ?????.\n\ + \n?? ??? ???? ????? ???? ???.\n\ + ??? ? ?? ?? ???? ?? ?????. + +# 2. Soft Delete ?? +# {0}: ??? ?? +notification.user.soft-delete.subject=[DGU AI LAB] ?? ??? ?? ???? ?? +notification.user.soft-delete.body=?????, {0}?.\n\ + \n?? ????? ?? ??? ????(?? ??) ???????.\n\ + \n???? ???? ?????, 1? ? ????? ???????? ?? ?????.\n\ + ?? ??? ???? ?? ????? ?? ????. \ No newline at end of file diff --git a/src/test/java/DGU_AI_LAB/admin_be/domain/scheduler/UserSchedulerServiceTest.java b/src/test/java/DGU_AI_LAB/admin_be/domain/scheduler/UserSchedulerServiceTest.java new file mode 100644 index 00000000..ec2cb7c4 --- /dev/null +++ b/src/test/java/DGU_AI_LAB/admin_be/domain/scheduler/UserSchedulerServiceTest.java @@ -0,0 +1,215 @@ +package DGU_AI_LAB.admin_be.domain.scheduler; + +import DGU_AI_LAB.admin_be.domain.alarm.service.AlarmService; +import DGU_AI_LAB.admin_be.domain.containerImage.entity.ContainerImage; +import DGU_AI_LAB.admin_be.domain.containerImage.repository.ContainerImageRepository; +import DGU_AI_LAB.admin_be.domain.requests.entity.Request; +import DGU_AI_LAB.admin_be.domain.requests.repository.RequestRepository; +import DGU_AI_LAB.admin_be.domain.resourceGroups.entity.ResourceGroup; +import DGU_AI_LAB.admin_be.domain.resourceGroups.repository.ResourceGroupRepository; +import DGU_AI_LAB.admin_be.domain.users.entity.User; +import DGU_AI_LAB.admin_be.domain.users.repository.UserRepository; +import DGU_AI_LAB.admin_be.global.util.MessageUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class UserSchedulerServiceTest { + + @Autowired + private UserSchedulerService userSchedulerService; + + @Autowired + private UserRepository userRepository; + @Autowired + private RequestRepository requestRepository; + @Autowired + private ResourceGroupRepository resourceGroupRepository; + @Autowired + private ContainerImageRepository containerImageRepository; + + @Autowired + private MessageUtils messageUtils; + + @MockitoBean + private AlarmService alarmService; + + + @Test + @DisplayName("유저 수명주기 통합 테스트: 알림(D-7, D-1), Soft Delete, Hard Delete, 활동 유저 보호") + void userLifecycleScheduler_IntegrationTest() { + // --- Given --- + LocalDateTime now = LocalDateTime.now(); + + // 1. [정상 유저] + User activeUser = createUser("active@test.com", "ActiveUser"); + updateLastLogin(activeUser, now.minusDays(1)); + + // 2. [보호 유저] + User podUser = createUser("pod@test.com", "PodUser"); + updateLastLogin(podUser, now.minusMonths(4)); + createRequestForUser(podUser, now.minusDays(1)); + + // 3. [경고 대상 D-7] (Login = Now - 3개월 + 7일) + User d7User = createUser("d7@test.com", "D7User"); + LocalDateTime d7LoginDate = now.minusMonths(3).plusDays(7); + updateLastLogin(d7User, d7LoginDate); + + // D-7 예상 메시지 생성 + LocalDateTime d7DeleteDate = d7LoginDate.plusMonths(3); + String d7Subject = messageUtils.get("notification.user.delete-warning.subject", "7"); + String d7Body = messageUtils.get("notification.user.delete-warning.body", + "D7User", "7", d7DeleteDate.toLocalDate().toString()); + + + // 4. [경고 대상 D-1] (Login = Now - 3개월 + 1일) + User d1User = createUser("d1@test.com", "D1User"); + LocalDateTime d1LoginDate = now.minusMonths(3).plusDays(1); + updateLastLogin(d1User, d1LoginDate); + + // D-1 예상 메시지 생성 + LocalDateTime d1DeleteDate = d1LoginDate.plusMonths(3); + String d1Subject = messageUtils.get("notification.user.delete-warning.subject", "1"); + String d1Body = messageUtils.get("notification.user.delete-warning.body", + "D1User", "1", d1DeleteDate.toLocalDate().toString()); + + + // 5. [Soft Delete 대상] + User softTarget = createUser("soft@test.com", "SoftTarget"); + updateLastLogin(softTarget, now.minusMonths(3).minusDays(1)); + + // Soft Delete 예상 메시지 + String softSubject = messageUtils.get("notification.user.soft-delete.subject"); + String softBody = messageUtils.get("notification.user.soft-delete.body", "SoftTarget"); + + + // 6. [Hard Delete 대상] + User hardTarget = createUser("hard@test.com", "HardTarget"); + hardTarget.withdraw(); + updateDeletedAt(hardTarget, now.minusYears(1).minusDays(1)); + + // 7. [Hard Delete 미대상] + User hardSafe = createUser("hardsafe@test.com", "HardSafe"); + hardSafe.withdraw(); + updateDeletedAt(hardSafe, now.minusMonths(11)); + + + // --- When --- + userSchedulerService.runUserLifecycleScheduler(); + + + // --- Then --- + + // 1. 정상 유저 생존 + assertThat(userRepository.findById(activeUser.getUserId()).get().getIsActive()).isTrue(); + + // 2. 보호 유저 생존 + assertThat(userRepository.findById(podUser.getUserId()).get().getIsActive()).isTrue(); + + // 3. [D-7] 알림 검증 (정확한 메시지 매칭) + verify(alarmService).sendAllAlerts( + eq("D7User"), + eq("d7@test.com"), + eq(d7Subject), + eq(d7Body) + ); + + // 4. [D-1] 알림 검증 + verify(alarmService).sendAllAlerts( + eq("D1User"), + eq("d1@test.com"), + eq(d1Subject), + eq(d1Body) + ); + + // 5. [Soft Delete] 알림 검증 및 상태 확인 + User resSoft = userRepository.findById(softTarget.getUserId()).get(); + assertThat(resSoft.getIsActive()).isFalse(); + assertThat(resSoft.getDeletedAt()).isNotNull(); + + verify(alarmService).sendAllAlerts( + eq("SoftTarget"), + eq("soft@test.com"), + eq(softSubject), + eq(softBody) + ); + + // 6. [Hard Delete] 삭제 확인 + assertThat(userRepository.findById(hardTarget.getUserId())).isEmpty(); + + // 7. [Hard Delete 미대상] 생존 확인 + assertThat(userRepository.findById(hardSafe.getUserId())).isPresent(); + } + + + // --- Helper Methods --- + + private User createUser(String email, String name) { + return userRepository.save(User.builder() + .email(email) + .name(name) + .password("pw") + .studentId("1234") + .phone("010-0000-0000") + .department("CS") + .build()); + } + + private void updateLastLogin(User user, LocalDateTime time) { + try { + var field = User.class.getDeclaredField("lastLoginAt"); + field.setAccessible(true); + field.set(user, time); + userRepository.saveAndFlush(user); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void updateDeletedAt(User user, LocalDateTime time) { + try { + var field = User.class.getDeclaredField("deletedAt"); + field.setAccessible(true); + field.set(user, time); + userRepository.saveAndFlush(user); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void createRequestForUser(User user, LocalDateTime expiresAt) { + ResourceGroup rg = resourceGroupRepository.findAll().stream().findFirst() + .orElseGet(() -> resourceGroupRepository.save(ResourceGroup.builder().serverName("TestServer").resourceGroupName("G").build())); + ContainerImage img = containerImageRepository.findAll().stream().findFirst() + .orElseGet(() -> containerImageRepository.save(ContainerImage.builder().imageName("cuda").imageVersion("1").cudaVersion("1").description("d").build())); + + Request req = Request.builder() + .user(user) + .ubuntuUsername("user_" + user.getUserId()) + .ubuntuPassword("pw") + .volumeSizeGiB(10L) + .expiresAt(expiresAt) + .usagePurpose("test") + .formAnswers("{}") + .resourceGroup(rg) + .containerImage(img) + .build(); + + req.approve(img, rg, 10L, "approved"); + requestRepository.saveAndFlush(req); + } +} \ No newline at end of file