fix(import): add @Schema annotations and fix IOException test coverage

- Add @Schema(requiredMode = REQUIRED) to SkippedFile and ImportStatus
  record components so TypeScript codegen produces non-optional fields
  when generate:api is next run
- Extract openFileStream(File) as package-private method so the
  IOException path can be tested deterministically without relying on
  OS-level file permissions (which are bypassed when running as root)
- Replace assumeTrue-based IOException test with Mockito spy that stubs
  openFileStream — test now runs in CI unconditionally (45 tests, 0 skipped)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-18 15:12:02 +02:00
committed by marcel
parent e312cce4e1
commit 0e95bd9160
2 changed files with 30 additions and 16 deletions

View File

@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.importing;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
@@ -55,9 +56,20 @@ public class MassImportService {
public enum State { IDLE, RUNNING, DONE, FAILED } public enum State { IDLE, RUNNING, DONE, FAILED }
public record SkippedFile(String filename, String reason) {} public record SkippedFile(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String filename,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String reason
) {}
public record ImportStatus(State state, String statusCode, @JsonIgnore String message, int processed, List<SkippedFile> skippedFiles, LocalDateTime startedAt) { public record ImportStatus(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) State state,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String statusCode,
@JsonIgnore String message,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) int processed,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) List<SkippedFile> skippedFiles,
LocalDateTime startedAt
) {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("skipped") @JsonProperty("skipped")
public int skipped() { return skippedFiles.size(); } public int skipped() { return skippedFiles.size(); }
} }
@@ -300,8 +312,12 @@ public class MassImportService {
return new ProcessResult(processed, skippedFiles); return new ProcessResult(processed, skippedFiles);
} }
InputStream openFileStream(File file) throws IOException {
return new FileInputStream(file);
}
private boolean isPdfMagicBytes(File file) throws IOException { private boolean isPdfMagicBytes(File file) throws IOException {
try (InputStream is = new FileInputStream(file)) { try (InputStream is = openFileStream(file)) {
byte[] header = is.readNBytes(4); byte[] header = is.readNBytes(4);
return header.length == 4 return header.length == 4
&& header[0] == 0x25 // % && header[0] == 0x25 // %

View File

@@ -40,7 +40,6 @@ import java.util.zip.ZipOutputStream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -572,20 +571,19 @@ class MassImportServiceTest {
@Test @Test
void runImportAsync_skipsFile_whenMagicBytesCheckThrowsIOException(@TempDir Path tempDir) throws Exception { void runImportAsync_skipsFile_whenMagicBytesCheckThrowsIOException(@TempDir Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("unreadable.pdf"), "some content"); Files.writeString(tempDir.resolve("unreadable.pdf"), "some content");
File unreadable = tempDir.resolve("unreadable.pdf").toFile();
buildMinimalImportXlsx(tempDir, "unreadable.pdf"); buildMinimalImportXlsx(tempDir, "unreadable.pdf");
ReflectionTestUtils.setField(service, "importDir", tempDir.toString()); ReflectionTestUtils.setField(service, "importDir", tempDir.toString());
unreadable.setReadable(false); lenient().when(documentService.findByOriginalFilename(any())).thenReturn(Optional.empty());
assumeTrue(!unreadable.canRead(), "Requires non-root file permissions");
try { MassImportService spyService = spy(service);
service.runImportAsync(); doThrow(new java.io.IOException("simulated read error")).when(spyService).openFileStream(any(File.class));
assertThat(service.getStatus().skipped()).isEqualTo(1);
assertThat(service.getStatus().skippedFiles()) spyService.runImportAsync();
.extracting(MassImportService.SkippedFile::reason)
.containsExactly("FILE_READ_ERROR"); assertThat(spyService.getStatus().skipped()).isEqualTo(1);
} finally { assertThat(spyService.getStatus().skippedFiles())
unreadable.setReadable(true); .extracting(MassImportService.SkippedFile::reason)
} .containsExactly("FILE_READ_ERROR");
} }
// ─── readOds — XXE security regression ─────────────────────────────────── // ─── readOds — XXE security regression ───────────────────────────────────