Skip to content

Commit a4130ca

Browse files
authored
Merge pull request #78 from thughari/dev
active background
2 parents abc472c + ae58048 commit a4130ca

7 files changed

Lines changed: 117 additions & 76 deletions

File tree

backend/service.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,9 @@ spec:
160160
valueFrom:
161161
secretKeyRef:
162162
name: google-pubsub-service-account
163+
key: latest
164+
- name: CRON_SECRET
165+
valueFrom:
166+
secretKeyRef:
167+
name: cron-secret
163168
key: latest

backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@
1010

1111
import jakarta.transaction.Transactional;
1212
import lombok.extern.slf4j.Slf4j;
13-
import org.springframework.scheduling.annotation.Scheduled;
14-
import org.springframework.stereotype.Component;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.RequestHeader;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RestController;
20+
import org.springframework.web.server.ResponseStatusException;
1521

1622
import java.time.LocalDateTime;
1723
import java.util.List;
1824

19-
@Component
25+
@RestController
26+
@RequestMapping("/api/cron")
2027
@Slf4j
2128
public class JobScheduler {
2229

@@ -28,6 +35,9 @@ public class JobScheduler {
2835

2936
private final PasswordResetTokenRepository passwordTokenRepo;
3037
private final VerificationTokenRepository verificationTokenRepo;
38+
39+
@Value("${app.cron.secret}")
40+
private String expectedCronSecret;
3141

3242
public JobScheduler(JobService jobService,
3343
UserRepository userRepository,
@@ -44,66 +54,77 @@ public JobScheduler(JobService jobService,
4454
}
4555

4656
/**
47-
* Daily Maintenance: Rejects stale applications (>60 days).
48-
* Runs at Midnight UTC.
57+
* Daily Maintenance Endpoint: Replaces all internal @Scheduled jobs.
58+
* Validates the X-Cron-Secret header and executes jobs sequentially.
4959
*/
50-
@Scheduled(cron = "0 0 0 * * *")
51-
public void runStaleJobCleanup() {
52-
log.info("Maintenance: Starting stale job cleanup...");
60+
@PostMapping("/daily-maintenance")
61+
@Transactional
62+
public ResponseEntity<String> runDailyMaintenance(@RequestHeader(value = "X-Cron-Secret", required = false) String cronSecret) {
63+
if (cronSecret == null || !cronSecret.equals(expectedCronSecret)) {
64+
log.warn("Unauthorized access attempt to daily-maintenance cron endpoint");
65+
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid cron secret");
66+
}
67+
68+
log.info("Starting Daily Maintenance cron jobs...");
69+
70+
try {
71+
runStaleJobCleanup();
72+
renewGmailWatches();
73+
runSystemCleanup();
74+
processScheduledDeletions();
75+
76+
log.info("Daily Maintenance cron jobs completed successfully.");
77+
return ResponseEntity.ok("Maintenance completed successfully.");
78+
} catch (Exception e) {
79+
log.error("Error during daily maintenance execution: {}", e.getMessage(), e);
80+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Maintenance failed");
81+
}
82+
}
83+
84+
private void runStaleJobCleanup() {
85+
log.info("Maintenance Step 1: Starting stale job cleanup...");
5386
try {
5487
jobService.cleanupStaleApplications();
55-
log.info("Maintenance: Stale job cleanup completed.");
88+
log.info("Maintenance Step 1: Stale job cleanup completed.");
5689
} catch (Exception e) {
57-
log.error("Maintenance Error: Stale job cleanup failed: {}", e.getMessage());
90+
log.error("Maintenance Step 1 Error: Stale job cleanup failed: {}", e.getMessage());
5891
}
5992
}
6093

61-
/**
62-
* Gmail Security: Renews the 7-day watch lease every 5 days.
63-
*/
64-
@Scheduled(cron = "0 30 0 */5 * *")
65-
public void renewGmailWatches() {
66-
log.info("Gmail Sync: Starting bulk watch renewal...");
94+
private void renewGmailWatches() {
95+
log.info("Maintenance Step 2: Starting bulk watch renewal...");
6796

6897
List<User> users = userRepository.findByGmailConnectedTrue();
6998

7099
if (users.isEmpty()) {
71-
log.info("Gmail Sync: No connected users found for renewal.");
100+
log.info("Maintenance Step 2: No connected users found for renewal.");
72101
return;
73102
}
74103

75104
users.parallelStream().forEach(user -> {
76105
try {
77106
gmailIntegrationService.renewWatch(user);
78107
} catch (Exception e) {
79-
log.error("Gmail Sync Error: Renewal failed for {}: {}", user.getEmail(), e.getMessage());
108+
log.error("Maintenance Step 2 Error: Renewal failed for {}: {}", user.getEmail(), e.getMessage());
80109
}
81110
});
82111

83-
log.info("Gmail Sync: Finished bulk watch renewal for {} users.", users.size());
112+
log.info("Maintenance Step 2: Finished bulk watch renewal for {} users.", users.size());
84113
}
85114

86-
@Scheduled(cron = "0 0 1 * * *")
87-
@Transactional
88-
public void runSystemCleanup() {
89-
log.info("Starting system-wide security cleanup...");
115+
private void runSystemCleanup() {
116+
log.info("Maintenance Step 3: Starting system-wide security cleanup...");
90117
LocalDateTime now = LocalDateTime.now();
91118

92119
passwordTokenRepo.deleteAllExpired(now);
93-
94120
verificationTokenRepo.deleteAllExpired(now);
95-
96121
userRepository.deleteUnverifiedUsers(now.minusDays(3));
97122

98-
log.info("System cleanup completed. Database pruned of expired security entries.");
123+
log.info("Maintenance Step 3: System cleanup completed. Database pruned of expired security entries.");
99124
}
100125

101-
/*
102-
* Scheduled task to process user deletions after the 3-day grace period.
103-
*/
104-
@Scheduled(cron = "0 30 1 * * *")
105-
public void processScheduledDeletions() {
106-
log.info("Starting scheduled user deletion cleanup...");
126+
private void processScheduledDeletions() {
127+
log.info("Maintenance Step 4: Starting scheduled user deletion cleanup...");
107128

108129
List<User> usersToDelete = userRepository.findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(
109130
LocalDateTime.now().minusDays(3)
@@ -113,10 +134,10 @@ public void processScheduledDeletions() {
113134
try {
114135
userDeletionService.deleteUserCompletely(user.getEmail());
115136
} catch (Exception e) {
116-
log.error("Failed to delete user: {}", user.getEmail(), e);
137+
log.error("Maintenance Step 4 Error: Failed to delete user: {}", user.getEmail(), e);
117138
}
118139
}
119140

120-
log.info("Scheduled deletion cleanup completed. Deleted {} users.", usersToDelete.size());
141+
log.info("Maintenance Step 4: Scheduled deletion cleanup completed. Deleted {} users.", usersToDelete.size());
121142
}
122143
}

backend/src/main/resources/application-dev.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/o
3636
# config
3737
app.allowed.cors=http://localhost:4200
3838
app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS
39-
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
39+
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/**
40+
app.cron.secret=${CRON_SECRET:default-dev-cron-secret-123}
4041

4142
# JWT Secret (Must be long and secure)
4243
app.jwt.secret=${JWT_SECRET}

backend/src/main/resources/application-local.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ app.jwt.refresh-cookie-same-site=Lax
3535
# config
3636
app.allowed.cors=http://localhost:4200
3737
app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS
38-
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/**
38+
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/**
39+
app.cron.secret=local-cron-secret-123
3940

4041
# Hibernate
4142
spring.jpa.hibernate.ddl-auto=update

backend/src/main/resources/application-prod.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/o
5151
# config
5252
app.allowed.cors=https://jobtrackerpro.in
5353
app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS
54-
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
54+
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/**
55+
app.cron.secret=${CRON_SECRET}
5556

5657
spring.threads.virtual.enabled=true
5758

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package com.thughari.jobtrackerpro.scheduler;
22

33
import com.thughari.jobtrackerpro.entity.User;
4+
import com.thughari.jobtrackerpro.repo.PasswordResetTokenRepository;
45
import com.thughari.jobtrackerpro.repo.UserRepository;
6+
import com.thughari.jobtrackerpro.repo.VerificationTokenRepository;
57
import com.thughari.jobtrackerpro.service.GmailIntegrationService;
68
import com.thughari.jobtrackerpro.service.JobService;
9+
import com.thughari.jobtrackerpro.service.UserDeletionService;
10+
import org.junit.jupiter.api.BeforeEach;
711
import org.junit.jupiter.api.Test;
812
import org.junit.jupiter.api.extension.ExtendWith;
913
import org.mockito.InjectMocks;
1014
import org.mockito.Mock;
1115
import org.mockito.junit.jupiter.MockitoExtension;
16+
import org.springframework.test.util.ReflectionTestUtils;
17+
import org.springframework.web.server.ResponseStatusException;
1218

1319
import java.util.List;
1420

21+
import static org.junit.jupiter.api.Assertions.assertThrows;
1522
import static org.mockito.Mockito.*;
1623

1724
@ExtendWith(MockitoExtension.class)
@@ -20,73 +27,77 @@ class JobSchedulerTest {
2027
@Mock private JobService jobService;
2128
@Mock private UserRepository userRepository;
2229
@Mock private GmailIntegrationService gmailIntegrationService;
30+
@Mock private UserDeletionService userDeletionService;
31+
@Mock private PasswordResetTokenRepository passwordTokenRepo;
32+
@Mock private VerificationTokenRepository verificationTokenRepo;
2333

2434
@InjectMocks
2535
private JobScheduler scheduler;
2636

27-
@Test
28-
void runStaleJobCleanup_InvokesService() {
29-
scheduler.runStaleJobCleanup();
30-
verify(jobService, times(1)).cleanupStaleApplications();
37+
private final String VALID_SECRET = "test-secret";
38+
39+
@BeforeEach
40+
void setUp() {
41+
ReflectionTestUtils.setField(scheduler, "expectedCronSecret", VALID_SECRET);
3142
}
3243

3344
@Test
34-
void runStaleJobCleanup_HandlesServiceException() {
35-
// Verification that an exception in the service doesn't propagate and crash the scheduler thread
36-
doThrow(new RuntimeException("DB Timeout")).when(jobService).cleanupStaleApplications();
37-
38-
scheduler.runStaleJobCleanup();
39-
40-
verify(jobService).cleanupStaleApplications();
45+
void runDailyMaintenance_UnauthorizedWhenSecretIsMissingOrInvalid() {
46+
assertThrows(ResponseStatusException.class, () -> scheduler.runDailyMaintenance(null));
47+
assertThrows(ResponseStatusException.class, () -> scheduler.runDailyMaintenance("wrong-secret"));
4148
}
4249

4350
@Test
44-
void renewGmailWatches_ProcessesAllConnectedUsers() {
45-
// Setup: Mocking connected users
51+
void runDailyMaintenance_ExecutesAllJobsSequentially() {
52+
// Setup users for Gmail watch renewal
4653
User user1 = new User();
4754
user1.setEmail("user1@test.com");
48-
User user2 = new User();
49-
user2.setEmail("user2@test.com");
50-
51-
when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of(user1, user2));
55+
when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of(user1));
5256

5357
// Act
54-
scheduler.renewGmailWatches();
58+
scheduler.runDailyMaintenance(VALID_SECRET);
5559

56-
// Assert: High Performance check
57-
// Verify that the integration service was called for every user returned by the repo
60+
// Assert Step 1: Stale Job Cleanup
61+
verify(jobService, times(1)).cleanupStaleApplications();
62+
63+
// Assert Step 2: Gmail Sync
5864
verify(gmailIntegrationService, times(1)).renewWatch(user1);
59-
verify(gmailIntegrationService, times(1)).renewWatch(user2);
65+
66+
// Assert Step 3: System Cleanup
67+
verify(passwordTokenRepo, times(1)).deleteAllExpired(any());
68+
verify(verificationTokenRepo, times(1)).deleteAllExpired(any());
69+
verify(userRepository, times(1)).deleteUnverifiedUsers(any());
70+
71+
// Assert Step 4: Scheduled Deletions
72+
verify(userRepository, times(1)).findAllByPendingDeletionTrueAndDeletionRequestedAtBefore(any());
6073
}
6174

6275
@Test
63-
void renewGmailWatches_HandlesPartialFailures() {
64-
// Setup: One user succeeds, one fails
76+
void runDailyMaintenance_HandlesPartialFailuresAcrossJobs() {
77+
// Verification that an exception in one step doesn't crash the entire maintenance run
78+
79+
// Step 1 throws error
80+
doThrow(new RuntimeException("DB Timeout")).when(jobService).cleanupStaleApplications();
81+
82+
// Step 2 mock setup
6583
User user1 = new User();
6684
user1.setEmail("fail@test.com");
6785
User user2 = new User();
6886
user2.setEmail("success@test.com");
69-
7087
when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of(user1, user2));
71-
72-
// Mocking an error for the first user
7388
doThrow(new RuntimeException("Token Revoked")).when(gmailIntegrationService).renewWatch(user1);
7489

7590
// Act
76-
scheduler.renewGmailWatches();
91+
scheduler.runDailyMaintenance(VALID_SECRET);
7792

78-
// Assert: Robustness check
79-
// Even though user1 failed, user2 MUST still be processed (Fault Tolerance)
93+
// Assert: Step 1 executed and failed
94+
verify(jobService).cleanupStaleApplications();
95+
96+
// Assert: Step 2 still executed, and even though user1 failed, user2 MUST still be processed
8097
verify(gmailIntegrationService).renewWatch(user1);
8198
verify(gmailIntegrationService).renewWatch(user2);
82-
}
83-
84-
@Test
85-
void renewGmailWatches_SkipsIfNoUsersConnected() {
86-
when(userRepository.findByGmailConnectedTrue()).thenReturn(List.of());
87-
88-
scheduler.renewGmailWatches();
89-
90-
verify(gmailIntegrationService, never()).renewWatch(any());
99+
100+
// Assert: Subsequent steps still run
101+
verify(passwordTokenRepo, times(1)).deleteAllExpired(any());
91102
}
92103
}

backend/src/test/resources/application-test.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ app.storage.type=local
1111

1212
app.allowed.cors=http://localhost:4200
1313
app.allowed.methods=GET,POST,PUT,DELETE,OPTIONS
14-
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/**
14+
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/**
15+
app.cron.secret=test-cron-secret-123
1516

1617
app.jwt.secret=012345678901234567890123456789012345678901234567890123456789
1718
app.jwt.expiration-ms=900000

0 commit comments

Comments
 (0)