From 0cd1f2b3b8cf0f916c9100c75088dc47f0a060de Mon Sep 17 00:00:00 2001 From: Hari Date: Thu, 11 Jun 2026 11:10:08 +0530 Subject: [PATCH 1/2] active background --- backend/service.yaml | 5 ++ .../jobtrackerpro/scheduler/JobScheduler.java | 89 ++++++++++++------- .../main/resources/application-dev.properties | 3 +- .../resources/application-local.properties | 3 +- .../resources/application-prod.properties | 3 +- 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/backend/service.yaml b/backend/service.yaml index 44133b4..72e68e5 100644 --- a/backend/service.yaml +++ b/backend/service.yaml @@ -160,4 +160,9 @@ spec: valueFrom: secretKeyRef: name: google-pubsub-service-account + key: latest + - name: CRON_SECRET + valueFrom: + secretKeyRef: + name: cron-secret key: latest \ No newline at end of file diff --git a/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java b/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java index 7ac9af6..527d936 100644 --- a/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java +++ b/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java @@ -10,13 +10,20 @@ import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; import java.util.List; -@Component +@RestController +@RequestMapping("/api/cron") @Slf4j public class JobScheduler { @@ -28,6 +35,9 @@ public class JobScheduler { private final PasswordResetTokenRepository passwordTokenRepo; private final VerificationTokenRepository verificationTokenRepo; + + @Value("${app.cron.secret}") + private String expectedCronSecret; public JobScheduler(JobService jobService, UserRepository userRepository, @@ -44,31 +54,50 @@ public JobScheduler(JobService jobService, } /** - * Daily Maintenance: Rejects stale applications (>60 days). - * Runs at Midnight UTC. + * Daily Maintenance Endpoint: Replaces all internal @Scheduled jobs. + * Validates the X-Cron-Secret header and executes jobs sequentially. */ - @Scheduled(cron = "0 0 0 * * *") - public void runStaleJobCleanup() { - log.info("Maintenance: Starting stale job cleanup..."); + @PostMapping("/daily-maintenance") + @Transactional + public ResponseEntity runDailyMaintenance(@RequestHeader(value = "X-Cron-Secret", required = false) String cronSecret) { + if (cronSecret == null || !cronSecret.equals(expectedCronSecret)) { + log.warn("Unauthorized access attempt to daily-maintenance cron endpoint"); + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid cron secret"); + } + + log.info("Starting Daily Maintenance cron jobs..."); + + try { + runStaleJobCleanup(); + renewGmailWatches(); + runSystemCleanup(); + processScheduledDeletions(); + + log.info("Daily Maintenance cron jobs completed successfully."); + return ResponseEntity.ok("Maintenance completed successfully."); + } catch (Exception e) { + log.error("Error during daily maintenance execution: {}", e.getMessage(), e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Maintenance failed"); + } + } + + private void runStaleJobCleanup() { + log.info("Maintenance Step 1: Starting stale job cleanup..."); try { jobService.cleanupStaleApplications(); - log.info("Maintenance: Stale job cleanup completed."); + log.info("Maintenance Step 1: Stale job cleanup completed."); } catch (Exception e) { - log.error("Maintenance Error: Stale job cleanup failed: {}", e.getMessage()); + log.error("Maintenance Step 1 Error: Stale job cleanup failed: {}", e.getMessage()); } } - /** - * Gmail Security: Renews the 7-day watch lease every 5 days. - */ - @Scheduled(cron = "0 30 0 */5 * *") - public void renewGmailWatches() { - log.info("Gmail Sync: Starting bulk watch renewal..."); + private void renewGmailWatches() { + log.info("Maintenance Step 2: Starting bulk watch renewal..."); List users = userRepository.findByGmailConnectedTrue(); if (users.isEmpty()) { - log.info("Gmail Sync: No connected users found for renewal."); + log.info("Maintenance Step 2: No connected users found for renewal."); return; } @@ -76,34 +105,26 @@ public void renewGmailWatches() { try { gmailIntegrationService.renewWatch(user); } catch (Exception e) { - log.error("Gmail Sync Error: Renewal failed for {}: {}", user.getEmail(), e.getMessage()); + log.error("Maintenance Step 2 Error: Renewal failed for {}: {}", user.getEmail(), e.getMessage()); } }); - log.info("Gmail Sync: Finished bulk watch renewal for {} users.", users.size()); + log.info("Maintenance Step 2: Finished bulk watch renewal for {} users.", users.size()); } - @Scheduled(cron = "0 0 1 * * *") - @Transactional - public void runSystemCleanup() { - log.info("Starting system-wide security cleanup..."); + private void runSystemCleanup() { + log.info("Maintenance Step 3: Starting system-wide security cleanup..."); LocalDateTime now = LocalDateTime.now(); passwordTokenRepo.deleteAllExpired(now); - verificationTokenRepo.deleteAllExpired(now); - userRepository.deleteUnverifiedUsers(now.minusDays(3)); - log.info("System cleanup completed. Database pruned of expired security entries."); + log.info("Maintenance Step 3: System cleanup completed. Database pruned of expired security entries."); } - /* - * Scheduled task to process user deletions after the 3-day grace period. - */ - @Scheduled(cron = "0 30 1 * * *") - public void processScheduledDeletions() { - log.info("Starting scheduled user deletion cleanup..."); + private void processScheduledDeletions() { + log.info("Maintenance Step 4: Starting scheduled user deletion cleanup..."); List usersToDelete = userRepository.findAllByPendingDeletionTrueAndDeletionRequestedAtBefore( LocalDateTime.now().minusDays(3) @@ -113,10 +134,10 @@ public void processScheduledDeletions() { try { userDeletionService.deleteUserCompletely(user.getEmail()); } catch (Exception e) { - log.error("Failed to delete user: {}", user.getEmail(), e); + log.error("Maintenance Step 4 Error: Failed to delete user: {}", user.getEmail(), e); } } - log.info("Scheduled deletion cleanup completed. Deleted {} users.", usersToDelete.size()); + log.info("Maintenance Step 4: Scheduled deletion cleanup completed. Deleted {} users.", usersToDelete.size()); } } \ No newline at end of file diff --git a/backend/src/main/resources/application-dev.properties b/backend/src/main/resources/application-dev.properties index b8b8ce2..03b5be0 100644 --- a/backend/src/main/resources/application-dev.properties +++ b/backend/src/main/resources/application-dev.properties @@ -36,7 +36,8 @@ spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/o # config app.allowed.cors=http://localhost:4200 app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS -app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/verify-email,/api/auth/resend-verification,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/webhooks/gmail/push +app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/verify-email,/api/auth/resend-verification,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/webhooks/gmail/push,/api/cron/** +app.cron.secret=${CRON_SECRET:default-dev-cron-secret-123} # JWT Secret (Must be long and secure) app.jwt.secret=${JWT_SECRET} diff --git a/backend/src/main/resources/application-local.properties b/backend/src/main/resources/application-local.properties index 584b8bb..34b2225 100644 --- a/backend/src/main/resources/application-local.properties +++ b/backend/src/main/resources/application-local.properties @@ -35,7 +35,8 @@ app.jwt.refresh-cookie-same-site=Lax # config app.allowed.cors=http://localhost:4200 app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS -app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/verify-email,/api/auth/resend-verification,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/webhooks/gmail/push,/api/storage/files/** +app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/verify-email,/api/auth/resend-verification,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/webhooks/gmail/push,/api/storage/files/**,/api/cron/** +app.cron.secret=local-cron-secret-123 # Hibernate spring.jpa.hibernate.ddl-auto=update diff --git a/backend/src/main/resources/application-prod.properties b/backend/src/main/resources/application-prod.properties index 372641e..0fe8f1a 100644 --- a/backend/src/main/resources/application-prod.properties +++ b/backend/src/main/resources/application-prod.properties @@ -51,7 +51,8 @@ spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/o # config app.allowed.cors=https://jobtrackerpro.in app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS -app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/verify-email,/api/auth/resend-verification,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/webhooks/gmail/push +app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/verify-email,/api/auth/resend-verification,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/webhooks/gmail/push,/api/cron/** +app.cron.secret=${CRON_SECRET} spring.threads.virtual.enabled=true From ae58048b0729e8df1b3f94c541da905a622a1d18 Mon Sep 17 00:00:00 2001 From: Hari Date: Thu, 11 Jun 2026 11:20:32 +0530 Subject: [PATCH 2/2] yup --- .../scheduler/JobSchedulerTest.java | 87 +++++++++++-------- .../resources/application-test.properties | 3 +- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java b/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java index eebc11c..30490d0 100644 --- a/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java +++ b/backend/src/test/java/com/thughari/jobtrackerpro/scheduler/JobSchedulerTest.java @@ -1,17 +1,24 @@ package com.thughari.jobtrackerpro.scheduler; import com.thughari.jobtrackerpro.entity.User; +import com.thughari.jobtrackerpro.repo.PasswordResetTokenRepository; import com.thughari.jobtrackerpro.repo.UserRepository; +import com.thughari.jobtrackerpro.repo.VerificationTokenRepository; import com.thughari.jobtrackerpro.service.GmailIntegrationService; import com.thughari.jobtrackerpro.service.JobService; +import com.thughari.jobtrackerpro.service.UserDeletionService; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.ResponseStatusException; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -20,73 +27,77 @@ class JobSchedulerTest { @Mock private JobService jobService; @Mock private UserRepository userRepository; @Mock private GmailIntegrationService gmailIntegrationService; + @Mock private UserDeletionService userDeletionService; + @Mock private PasswordResetTokenRepository passwordTokenRepo; + @Mock private VerificationTokenRepository verificationTokenRepo; @InjectMocks private JobScheduler scheduler; - @Test - void runStaleJobCleanup_InvokesService() { - scheduler.runStaleJobCleanup(); - verify(jobService, times(1)).cleanupStaleApplications(); + private final String VALID_SECRET = "test-secret"; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(scheduler, "expectedCronSecret", VALID_SECRET); } @Test - void runStaleJobCleanup_HandlesServiceException() { - // Verification that an exception in the service doesn't propagate and crash the scheduler thread - doThrow(new RuntimeException("DB Timeout")).when(jobService).cleanupStaleApplications(); - - scheduler.runStaleJobCleanup(); - - verify(jobService).cleanupStaleApplications(); + void runDailyMaintenance_UnauthorizedWhenSecretIsMissingOrInvalid() { + assertThrows(ResponseStatusException.class, () -> scheduler.runDailyMaintenance(null)); + assertThrows(ResponseStatusException.class, () -> scheduler.runDailyMaintenance("wrong-secret")); } @Test - void renewGmailWatches_ProcessesAllConnectedUsers() { - // Setup: Mocking connected users + void runDailyMaintenance_ExecutesAllJobsSequentially() { + // Setup users for Gmail watch renewal User user1 = new User(); user1.setEmail("user1@test.com"); - User user2 = new User(); - user2.setEmail("user2@test.com"); - - when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of(user1, user2)); + when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of(user1)); // Act - scheduler.renewGmailWatches(); + scheduler.runDailyMaintenance(VALID_SECRET); - // Assert: High Performance check - // Verify that the integration service was called for every user returned by the repo + // Assert Step 1: Stale Job Cleanup + verify(jobService, times(1)).cleanupStaleApplications(); + + // Assert Step 2: Gmail Sync verify(gmailIntegrationService, times(1)).renewWatch(user1); - verify(gmailIntegrationService, times(1)).renewWatch(user2); + + // Assert Step 3: System Cleanup + verify(passwordTokenRepo, times(1)).deleteAllExpired(any()); + verify(verificationTokenRepo, times(1)).deleteAllExpired(any()); + verify(userRepository, times(1)).deleteUnverifiedUsers(any()); + + // Assert Step 4: Scheduled Deletions + verify(userRepository, times(1)).findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(any()); } @Test - void renewGmailWatches_HandlesPartialFailures() { - // Setup: One user succeeds, one fails + void runDailyMaintenance_HandlesPartialFailuresAcrossJobs() { + // Verification that an exception in one step doesn't crash the entire maintenance run + + // Step 1 throws error + doThrow(new RuntimeException("DB Timeout")).when(jobService).cleanupStaleApplications(); + + // Step 2 mock setup User user1 = new User(); user1.setEmail("fail@test.com"); User user2 = new User(); user2.setEmail("success@test.com"); - when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of(user1, user2)); - - // Mocking an error for the first user doThrow(new RuntimeException("Token Revoked")).when(gmailIntegrationService).renewWatch(user1); // Act - scheduler.renewGmailWatches(); + scheduler.runDailyMaintenance(VALID_SECRET); - // Assert: Robustness check - // Even though user1 failed, user2 MUST still be processed (Fault Tolerance) + // Assert: Step 1 executed and failed + verify(jobService).cleanupStaleApplications(); + + // Assert: Step 2 still executed, and even though user1 failed, user2 MUST still be processed verify(gmailIntegrationService).renewWatch(user1); verify(gmailIntegrationService).renewWatch(user2); - } - - @Test - void renewGmailWatches_SkipsIfNoUsersConnected() { - when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of()); - - scheduler.renewGmailWatches(); - - verify(gmailIntegrationService, never()).renewWatch(any()); + + // Assert: Subsequent steps still run + verify(passwordTokenRepo, times(1)).deleteAllExpired(any()); } } \ No newline at end of file diff --git a/backend/src/test/resources/application-test.properties b/backend/src/test/resources/application-test.properties index 2280131..3495dda 100644 --- a/backend/src/test/resources/application-test.properties +++ b/backend/src/test/resources/application-test.properties @@ -11,7 +11,8 @@ app.storage.type=local app.allowed.cors=http://localhost:4200 app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS -app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/storage/files/** +app.public.endpoints=/api/auth/signup,/api/auth/login,/api/auth/refresh,/api/auth/logout,/api/auth/forgot-password,/api/auth/reset-password,/oauth2/**,/api/webhooks/inbound-email,/api/storage/files/**,/api/cron/** +app.cron.secret=test-cron-secret-123 app.jwt.secret=012345678901234567890123456789012345678901234567890123456789 app.jwt.expiration-ms=900000