|
32 | 32 | import java.time.LocalDate; |
33 | 33 | import java.util.Map; |
34 | 34 | import java.util.UUID; |
| 35 | +import java.util.stream.Stream; |
| 36 | +import org.apache.pdfbox.Loader; |
| 37 | +import org.apache.pdfbox.io.RandomAccessReadBuffer; |
| 38 | +import org.apache.pdfbox.pdmodel.PDDocument; |
| 39 | +import org.apache.pdfbox.text.PDFTextStripper; |
35 | 40 | import org.junit.jupiter.api.BeforeEach; |
36 | 41 | import org.junit.jupiter.api.Nested; |
37 | 42 | import org.junit.jupiter.api.Test; |
| 43 | +import org.junit.jupiter.params.ParameterizedTest; |
| 44 | +import org.junit.jupiter.params.provider.Arguments; |
| 45 | +import org.junit.jupiter.params.provider.MethodSource; |
38 | 46 | import org.springframework.beans.factory.annotation.Autowired; |
39 | 47 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
40 | 48 | import org.springframework.boot.test.context.SpringBootTest; |
@@ -76,7 +84,6 @@ class PDFExportResourceTest extends AbstractResourceTest { |
76 | 84 | Applicant applicant; |
77 | 85 | Applicant applicantWithWebsiteAndLinkedin; |
78 | 86 | Job job; |
79 | | - Job jobWithNulls; |
80 | 87 | Application application; |
81 | 88 | Application applicationWithWebsiteAndLinkedin; |
82 | 89 |
|
@@ -214,6 +221,16 @@ private ApplicationPDFRequest createApplicationPdfRequest(Application applicatio |
214 | 221 | return new ApplicationPDFRequest(appDto, labels); |
215 | 222 | } |
216 | 223 |
|
| 224 | + // Helper to extract text from a PDF byte array using PDFBox |
| 225 | + private String extractTextFromPdf(byte[] pdfBytes) { |
| 226 | + try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBuffer(pdfBytes))) { |
| 227 | + PDFTextStripper stripper = new PDFTextStripper(); |
| 228 | + return stripper.getText(doc); |
| 229 | + } catch (Exception e) { |
| 230 | + throw new RuntimeException("Failed to extract text from PDF", e); |
| 231 | + } |
| 232 | + } |
| 233 | + |
217 | 234 | @Nested |
218 | 235 | class ExportApplicationToPDF { |
219 | 236 |
|
@@ -268,7 +285,7 @@ void shouldExportApplicationWithMasterDegreeNameNull() { |
268 | 285 | "Test", |
269 | 286 | "Test" |
270 | 287 | ); |
271 | | - ApplicationPDFRequest request = createApplicationPdfRequest(application, job); |
| 288 | + ApplicationPDFRequest request = createApplicationPdfRequest(appWithMaster, job); |
272 | 289 |
|
273 | 290 | byte[] result = asApplicant(applicantWithMasterNameNull).postAndReturnBytes( |
274 | 291 | BASE_URL + "/application/pdf", |
@@ -329,6 +346,60 @@ void shouldHandleJobWithNullFields() { |
329 | 346 |
|
330 | 347 | assertValidPdf(result); |
331 | 348 | } |
| 349 | + |
| 350 | + @ParameterizedTest |
| 351 | + @MethodSource("jobLanguageProvider") |
| 352 | + void exportJobToPDFLanguageSelection(String enDescription, String deDescription, String requestedLang, String expectedSubstring) { |
| 353 | + Job jobToTest = JobTestData.savedAll( |
| 354 | + jobRepository, |
| 355 | + "Lang Test Job", |
| 356 | + "AI", |
| 357 | + "CS", |
| 358 | + professor, |
| 359 | + group, |
| 360 | + Campus.GARCHING, |
| 361 | + LocalDate.now(), |
| 362 | + LocalDate.now(), |
| 363 | + 20, |
| 364 | + 3, |
| 365 | + FundingType.FULLY_FUNDED, |
| 366 | + enDescription, |
| 367 | + deDescription, |
| 368 | + JobState.PUBLISHED |
| 369 | + ); |
| 370 | + |
| 371 | + Map<String, String> labels = createCompleteLabelsMap(); |
| 372 | + if (requestedLang != null) { |
| 373 | + labels.put("lang", requestedLang); |
| 374 | + } |
| 375 | + |
| 376 | + byte[] pdf = api |
| 377 | + .withoutPostProcessors() |
| 378 | + .postAndReturnBytes(BASE_URL + "/job/" + jobToTest.getJobId() + "/pdf", labels, 200, MediaType.APPLICATION_PDF); |
| 379 | + |
| 380 | + assertValidPdf(pdf); |
| 381 | + String text = extractTextFromPdf(pdf); |
| 382 | + assertThat(text).contains(expectedSubstring); |
| 383 | + } |
| 384 | + |
| 385 | + static Stream<Arguments> jobLanguageProvider() { |
| 386 | + return Stream.of( |
| 387 | + // both present -> select en |
| 388 | + Arguments.of("EN unique-en-xyz", "DE unique-de-xyz", "en", "unique-en-xyz"), |
| 389 | + // both present -> select de |
| 390 | + Arguments.of("EN unique-en-xyz", "DE unique-de-xyz", "de", "unique-de-xyz"), |
| 391 | + // only EN present -> requested DE should fallback to EN |
| 392 | + Arguments.of("EN only unique-en-only", null, "de", "unique-en-only"), |
| 393 | + // only DE present -> requested EN should fallback to DE |
| 394 | + Arguments.of(null, "DE only unique-de-only", "en", "unique-de-only"), |
| 395 | + // de whitespace with en present -> fallback to en |
| 396 | + Arguments.of("EN fallback unique-en-ws", " ", "de", "unique-en-ws"), |
| 397 | + // both empty -> '-' |
| 398 | + Arguments.of("", "", null, "-"), |
| 399 | + // none present -> expect '-' |
| 400 | + Arguments.of(null, null, null, "-") |
| 401 | + ); |
| 402 | + } |
332 | 403 | } |
333 | 404 |
|
334 | 405 | @Nested |
|
0 commit comments