package vip.mate.tool.image; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import vip.mate.workspace.core.service.ChatUploadLocationResolverTestSupport; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import java.util.Comparator; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the data-URL handling added to {@link ImageFileDownloader}. * *

Network-bound HTTP downloads are intentionally not exercised here — * the regression we care about is the silent failure that happened when a * provider returned a {@code data:image/png;base64,...} URL: callers fed * that into {@code HttpUtil.downloadFile}, which mangled it into something * like {@code file:/cwd/http:/data:image/...} and threw, so the image * never landed on disk and the assistant message rendered empty. * *

The downloader writes under {@code data/chat-uploads//...} * relative to the JVM's working directory; we sweep that directory after * each test so the run leaves no artefacts behind. */ @Tag("media-gen") class ImageFileDownloaderTest { private ImageFileDownloader downloader; private final String conv = "test-conv-" + System.nanoTime(); @BeforeEach void setUp() { downloader = new ImageFileDownloader(ChatUploadLocationResolverTestSupport.legacyDefault()); } @AfterEach void cleanup() throws IOException { Path dir = Paths.get("data", "chat-uploads", conv); if (!Files.exists(dir)) return; try (Stream walk = Files.walk(dir)) { walk.sorted(Comparator.reverseOrder()).forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); } } @Test @DisplayName("download writes the decoded bytes when given a base64 data URL") void download_baseDataUrl_writesDecodedBytes() throws Exception { // 1x1 transparent PNG — the smallest legal payload we can verify byte-for-byte byte[] pngBytes = new byte[]{ (byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n', 0, 0, 0, 13, 'I', 'H', 'D', 'R', 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 0x1F, 0x15, (byte) 0xC4, (byte) 0x89 }; String dataUrl = "data:image/png;base64," + Base64.getEncoder().encodeToString(pngBytes); Path saved = downloader.download(dataUrl, conv, "task1", 0); assertTrue(Files.exists(saved), "saved file must exist"); assertTrue(saved.getFileName().toString().endsWith(".png")); byte[] readBack = Files.readAllBytes(saved); assertArrayEquals(pngBytes, readBack, "stored bytes must match decoded payload"); } @Test @DisplayName("download picks extension from the data-URL media type") void download_extensionMatchesMediaType() throws Exception { Path png = downloader.download( "data:image/png;base64," + Base64.getEncoder().encodeToString(new byte[]{1, 2, 3}), conv, "ext-png", 0); assertTrue(png.getFileName().toString().endsWith(".png")); Path jpg = downloader.download( "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(new byte[]{4, 5, 6}), conv, "ext-jpg", 0); assertTrue(jpg.getFileName().toString().endsWith(".jpg")); Path webp = downloader.download( "data:image/webp;base64," + Base64.getEncoder().encodeToString(new byte[]{7, 8, 9}), conv, "ext-webp", 0); assertTrue(webp.getFileName().toString().endsWith(".webp")); // Unknown / missing media type → default to png Path fallback = downloader.download( "data:;base64," + Base64.getEncoder().encodeToString(new byte[]{0}), conv, "ext-fallback", 0); assertTrue(fallback.getFileName().toString().endsWith(".png")); } @Test @DisplayName("download accepts the percent-encoded body form (no ;base64)") void download_percentEncodedDataUrl() throws Exception { // The ";base64" form is the common one but RFC 2397 also allows a raw // (URL-encoded) body. Make sure both round-trip safely. String dataUrl = "data:image/png,hello%20world"; Path saved = downloader.download(dataUrl, conv, "raw", 0); assertEquals("hello world", Files.readString(saved)); } @Test @DisplayName("download rejects malformed data URLs cleanly") void download_malformedDataUrlIsRejected() { IOException ex = assertThrows(IOException.class, () -> downloader.download("data:image/png;base64", conv, "bad", 0)); assertTrue(ex.getMessage().contains("Malformed data URL"), "expected explanatory error, got: " + ex.getMessage()); } @Test @DisplayName("download rejects invalid base64 payloads with a wrapped IOException") void download_invalidBase64IsWrapped() { // !!! is not a legal base64 token IOException ex = assertThrows(IOException.class, () -> downloader.download("data:image/png;base64,!!!", conv, "badb64", 0)); assertTrue(ex.getMessage().toLowerCase().contains("base64")); } @Test @DisplayName("download rejects null URLs without leaking NPE") void download_nullIsRejected() { IOException ex = assertThrows(IOException.class, () -> downloader.download(null, conv, "null", 0)); assertTrue(ex.getMessage().contains("null")); } }