Skip to content

Commit 357c405

Browse files
committed
add tests
1 parent 5cd6e65 commit 357c405

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package it.aboutbits.springboot.emailservice.lib.application;
2+
3+
import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource;
4+
import it.aboutbits.springboot.emailservice.lib.EmailState;
5+
import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException;
6+
import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository;
7+
import it.aboutbits.springboot.emailservice.lib.model.Email;
8+
import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment;
9+
import it.aboutbits.springboot.emailservice.support.database.WithPostgres;
10+
import it.aboutbits.springboot.emailservice.support.database.factory.EmailFactory;
11+
import org.jspecify.annotations.NullMarked;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
16+
17+
import java.util.HashSet;
18+
import java.util.Set;
19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.Executors;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.stream.IntStream;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.mockito.ArgumentMatchers.anyLong;
26+
import static org.mockito.Mockito.doThrow;
27+
import static org.mockito.Mockito.times;
28+
import static org.mockito.Mockito.verify;
29+
30+
@SpringBootTest(properties = {
31+
"aboutbits.emailservice.scheduling.interval=30000"
32+
})
33+
@WithPostgres
34+
@NullMarked
35+
class CleanupAttachmentFilesTest {
36+
@MockitoBean
37+
AttachmentDataSource attachmentDataSource;
38+
39+
@Autowired
40+
EmailRepository emailRepository;
41+
42+
@Autowired
43+
CleanupAttachmentFiles cleanupAttachmentFiles;
44+
45+
@Test
46+
void givenSentUncleanedEmails_cleanupAttachments_shouldReleaseAndMarkCleaned() throws AttachmentException {
47+
persistCleanableEmail(100L);
48+
persistCleanableEmail(101L);
49+
persistCleanableEmail(102L);
50+
51+
cleanupAttachmentFiles.cleanupAttachments();
52+
53+
assertThat(emailRepository.findAll())
54+
.hasSize(3)
55+
.allMatch(Email::isAttachmentsCleaned);
56+
verify(attachmentDataSource, times(1)).releaseAttachment(100L);
57+
verify(attachmentDataSource, times(1)).releaseAttachment(101L);
58+
verify(attachmentDataSource, times(1)).releaseAttachment(102L);
59+
}
60+
61+
@Test
62+
void givenNonSentOrAlreadyCleanedRows_cleanupAttachments_shouldSkip() throws AttachmentException {
63+
persistEmail(200L, EmailState.PENDING, false);
64+
persistEmail(201L, EmailState.SENDING, false);
65+
persistEmail(202L, EmailState.ERROR, false);
66+
persistEmail(203L, EmailState.SENT, true);
67+
68+
cleanupAttachmentFiles.cleanupAttachments();
69+
70+
verify(attachmentDataSource, times(0)).releaseAttachment(anyLong());
71+
assertThat(emailRepository.findAll())
72+
.filteredOn(email -> email.getState() != EmailState.SENT)
73+
.allMatch(email -> !email.isAttachmentsCleaned());
74+
}
75+
76+
@Test
77+
void givenReleaseFails_cleanupAttachments_shouldResetFlagForRetry() throws AttachmentException {
78+
var email = persistCleanableEmail(300L);
79+
doThrow(new AttachmentException()).when(attachmentDataSource).releaseAttachment(300L);
80+
81+
cleanupAttachmentFiles.cleanupAttachments();
82+
83+
assertThat(emailRepository.findById(email.getId()))
84+
.get()
85+
.satisfies(reloaded -> assertThat(reloaded.isAttachmentsCleaned()).isFalse());
86+
}
87+
88+
@Test
89+
void givenConcurrentPasses_cleanupAttachments_shouldReleaseEachEmailExactlyOnce() throws Exception {
90+
var totalEmails = 25;
91+
var workerThreads = 4;
92+
var passesPerWorker = 10;
93+
for (var i = 0; i < totalEmails; i++) {
94+
persistCleanableEmail(1000L + i);
95+
}
96+
97+
var startGate = new CountDownLatch(1);
98+
var executor = Executors.newFixedThreadPool(workerThreads);
99+
try {
100+
IntStream.range(0, workerThreads).forEach(_ -> executor.submit(() -> {
101+
startGate.await();
102+
for (var i = 0; i < passesPerWorker; i++) {
103+
cleanupAttachmentFiles.cleanupAttachments();
104+
}
105+
return null;
106+
}));
107+
startGate.countDown();
108+
executor.shutdown();
109+
assertThat(executor.awaitTermination(60, TimeUnit.SECONDS)).isTrue();
110+
} finally {
111+
if (!executor.isTerminated()) {
112+
executor.shutdownNow();
113+
}
114+
}
115+
116+
assertThat(emailRepository.findAll())
117+
.hasSize(totalEmails)
118+
.allMatch(Email::isAttachmentsCleaned);
119+
// Exactly one release per file reference
120+
verify(attachmentDataSource, times(totalEmails)).releaseAttachment(anyLong());
121+
}
122+
123+
private Email persistCleanableEmail(long fileReference) {
124+
return persistEmail(fileReference, EmailState.SENT, false);
125+
}
126+
127+
private Email persistEmail(long fileReference, EmailState state, boolean attachmentsCleaned) {
128+
var email = EmailFactory.once()
129+
.state(state)
130+
.attachmentsCleaned(attachmentsCleaned)
131+
.build();
132+
133+
var attachment = new EmailAttachment();
134+
attachment.setEmail(email);
135+
attachment.setFileName("file.png");
136+
attachment.setContentType("image/png");
137+
attachment.setFileReference(fileReference);
138+
email.setAttachments(new HashSet<>(Set.of(attachment)));
139+
140+
return emailRepository.save(email);
141+
}
142+
}

0 commit comments

Comments
 (0)