diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6aae359..b69a0d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,15 +1,15 @@ name: Build and test on: - # Build PRs and branches. - pull_request_target: - types: [ opened, synchronize, reopened ] + pull_request: + types: [opened, synchronize, reopened, closed] paths-ignore: - .github/workflows/deploy-tagged.yml + push: branches: - - '**' + - "**" tags-ignore: - - '**' + - "**" paths-ignore: - .github/workflows/deploy-tagged.yml @@ -19,41 +19,49 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # https://en.wikipedia.org/wiki/Java_version_history - java: [ '8', '11' ] # LTS + java: ["8", "11"] steps: - name: Checkout uses: actions/checkout@v6 with: - ref: ${{ github.event.pull_request.head.sha }} + # For pull_request, github.sha is a merge commit; we want the PR head SHA. + # For push, github.sha is correct. + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - name: Setup JDK uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} - distribution: 'temurin' + distribution: temurin - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: - # The Gradle wrapper's version (already the default, putting it here to clarity) gradle-version: wrapper - # Removing unused files from Gradle User Home before saving to cache (i.e. older versions of gradle) gradle-home-cache-cleanup: true - # Cache downloaded JDKs in addition to the default directories. gradle-home-cache-includes: | caches notifications jdks - name: Build and test + env: + UPLOADCARE_PUBLIC_KEY: ${{ secrets.UPLOADCARE_PUBLIC_KEY }} + UPLOADCARE_SECRET_KEY: ${{ secrets.UPLOADCARE_SECRET_KEY }} run: ./gradlew build --stacktrace - publish-snapshot: - name: Publish snapshot to GitHub Packages + publish-pr-snapshot: + name: Publish PR snapshot to GitHub Packages runs-on: ubuntu-latest needs: build - if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/master') + + # Only publish for PRs from the same repo (forks won't have permissions to publish packages). + if: > + github.event_name == 'pull_request' && + github.event.action != 'closed' && + github.event.pull_request.head.repo.full_name == github.repository + permissions: contents: read packages: write @@ -63,30 +71,26 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + - name: Get base version id: get-version run: | BASE_VERSION=$(grep -iE "version([ ]*)=" gradle.properties | cut -f 2 -d "=" | tr -d '[:space:]') - # Strip any existing -SNAPSHOT suffix so we can re-apply it cleanly BASE_VERSION="${BASE_VERSION%-SNAPSHOT}" echo "base_version=${BASE_VERSION}" >> $GITHUB_OUTPUT - - name: Determine snapshot version + - name: Determine PR snapshot version id: snapshot-version run: | - if [ "${{ github.event_name }}" == "pull_request_target" ]; then - SNAPSHOT_VERSION="${{ steps.get-version.outputs.base_version }}-PR-${{ github.event.pull_request.number }}-SNAPSHOT" - else - SNAPSHOT_VERSION="${{ steps.get-version.outputs.base_version }}-SNAPSHOT" - fi + SNAPSHOT_VERSION="${{ steps.get-version.outputs.base_version }}-PR-${{ github.event.pull_request.number }}-SNAPSHOT" echo "snapshot_version=${SNAPSHOT_VERSION}" >> $GITHUB_OUTPUT - echo "Publishing snapshot version: ${SNAPSHOT_VERSION}" + echo "Publishing PR snapshot version: ${SNAPSHOT_VERSION}" - name: Setup JDK uses: actions/setup-java@v5 with: - java-version: 8 - distribution: 'temurin' + java-version: "8" + distribution: temurin - name: Setup Gradle uses: gradle/gradle-build-action@v2 @@ -98,8 +102,122 @@ jobs: notifications jdks - - name: Publish snapshot to GitHub Packages + - name: Publish PR snapshot to GitHub Packages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_ACTOR: ${{ github.actor }} run: ./gradlew publishReleasePublicationToGitHubPackagesRepository -Pversion=${{ steps.snapshot-version.outputs.snapshot_version }} --stacktrace + + publish-master-snapshot: + name: Publish master snapshot to GitHub Packages + runs-on: ubuntu-latest + needs: build + + # Publish on direct pushes to master (including merge commits). + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Get base version + id: get-version + run: | + BASE_VERSION=$(grep -iE "version([ ]*)=" gradle.properties | cut -f 2 -d "=" | tr -d '[:space:]') + BASE_VERSION="${BASE_VERSION%-SNAPSHOT}" + echo "base_version=${BASE_VERSION}" >> $GITHUB_OUTPUT + + - name: Determine master snapshot version + id: snapshot-version + run: | + SNAPSHOT_VERSION="${{ steps.get-version.outputs.base_version }}-SNAPSHOT" + echo "snapshot_version=${SNAPSHOT_VERSION}" >> $GITHUB_OUTPUT + echo "Publishing master snapshot version: ${SNAPSHOT_VERSION}" + + - name: Setup JDK + uses: actions/setup-java@v5 + with: + java-version: "8" + distribution: temurin + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: wrapper + gradle-home-cache-cleanup: true + gradle-home-cache-includes: | + caches + notifications + jdks + + - name: Publish master snapshot to GitHub Packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} + run: ./gradlew publishReleasePublicationToGitHubPackagesRepository -Pversion=${{ steps.snapshot-version.outputs.snapshot_version }} --stacktrace + + cleanup-merged-pr-snapshot: + name: Delete merged PR snapshot from GitHub Packages + runs-on: ubuntu-latest + + # Triggered by pull_request.closed (merge) into master + if: > + github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'master' + + permissions: + packages: write + + steps: + - name: Compute PR snapshot version + id: prver + run: | + # Must match the same scheme used in publish-pr-snapshot + # We need base version too: + BASE_VERSION=$(grep -iE "version([ ]*)=" gradle.properties | cut -f 2 -d "=" | tr -d '[:space:]') + BASE_VERSION="${BASE_VERSION%-SNAPSHOT}" + echo "version=${BASE_VERSION}-PR-${{ github.event.pull_request.number }}-SNAPSHOT" >> $GITHUB_OUTPUT + + - name: Delete package version via GitHub API + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + const package_type = "maven"; + + // TODO: IMPORTANT: + // Set this to the exact "Package name" as shown in the repo's Packages UI. + // For Maven packages this is often the artifactId, but verify. + const package_name = "YOUR_PACKAGE_NAME_HERE"; + + const versionName = "${{ steps.prver.outputs.version }}"; + + const versions = await github.paginate( + github.rest.packages.getAllPackageVersionsForPackageOwnedByRepo, + { owner, repo, package_type, package_name, per_page: 100 } + ); + + const match = versions.find(v => v.name === versionName); + if (!match) { + core.warning(`No package version found named "${versionName}" in ${package_type}/${package_name}. Nothing to delete.`); + return; + } + + await github.rest.packages.deletePackageVersionForRepoOwnedByOwner({ + owner, + repo, + package_type, + package_name, + package_version_id: match.id, + }); + + core.info(`Deleted ${package_type}/${package_name}@${versionName} (id=${match.id}).`); diff --git a/src/main/java/com/uploadcare/api/File.java b/src/main/java/com/uploadcare/api/File.java index 59a0778..3dd30e8 100644 --- a/src/main/java/com/uploadcare/api/File.java +++ b/src/main/java/com/uploadcare/api/File.java @@ -82,11 +82,35 @@ public boolean isImage() { } public ImageInfo getImageInfo() { - return fileData.imageInfo; + if (fileData.imageInfo != null) { + return fileData.imageInfo; + } + if (fileData.contentInfo != null) { + return fileData.contentInfo.image; + } + return null; } public VideoInfo getVideoInfo() { - return fileData.videoInfo; + if (fileData.videoInfo != null) { + return fileData.videoInfo; + } + if (fileData.contentInfo != null) { + return fileData.contentInfo.video; + } + return null; + } + + public ContentInfo getContentInfo() { + return fileData.contentInfo; + } + + public Map getMetadata() { + return fileData.metadata; + } + + public Map getAppData() { + return fileData.appdata; } public Map getRekognitionInfo() { @@ -260,4 +284,51 @@ public String toString() { public enum ColorMode { RGB, RGBA, RGBa, RGBX, L, LA, La, P, PA, CMYK, YCbCr, HSV, LAB } + + public static class MimeInfo { + public String mime; + public String type; + public String subtype; + + @Override + public String toString() { + return "MimeInfo{" + + "mime='" + mime + '\'' + + ", type='" + type + '\'' + + ", subtype='" + subtype + '\'' + + '}'; + } + } + + public static class ContentInfo { + public MimeInfo mime; + public ImageInfo image; + public VideoInfo video; + + @Override + public String toString() { + return "ContentInfo{" + + "mime=" + mime + + ", image=" + image + + ", video=" + video + + '}'; + } + } + + public static class AppData { + public Map data; + public String version; + public Date datetimeCreated; + public Date datetimeUpdated; + + @Override + public String toString() { + return "AppData{" + + "data=" + data + + ", version='" + version + '\'' + + ", datetimeCreated=" + datetimeCreated + + ", datetimeUpdated=" + datetimeUpdated + + '}'; + } + } } diff --git a/src/main/java/com/uploadcare/api/RequestHelper.java b/src/main/java/com/uploadcare/api/RequestHelper.java index dda289e..9cd1a6d 100644 --- a/src/main/java/com/uploadcare/api/RequestHelper.java +++ b/src/main/java/com/uploadcare/api/RequestHelper.java @@ -36,7 +36,7 @@ import static com.uploadcare.urls.UrlUtils.trustedBuild; /** - * A helper class for doing API calls to the Uploadcare API. Supports API version 0.6. + * A helper class for doing API calls to the Uploadcare API. Supports API version 0.7. * * TODO Support of throttled requests needs to be added */ @@ -117,7 +117,7 @@ public void setApiHeaders(HttpUriRequest request, String requestBodyMD5) { String formattedDate = rfc2822(calendar.getTime()); request.addHeader("Content-Type", JSON_CONTENT_TYPE); - request.setHeader("Accept", "application/vnd.uploadcare-v0.6+json"); + request.setHeader("Accept", "application/vnd.uploadcare-v0.7+json"); request.setHeader("Date", formattedDate); request.setHeader("User-Agent", String.format("javauploadcare/%s/%s", LIBRARY_VERSION, client.getPublicKey())); diff --git a/src/main/java/com/uploadcare/data/FileData.java b/src/main/java/com/uploadcare/data/FileData.java index 5ccd73d..64164d4 100644 --- a/src/main/java/com/uploadcare/data/FileData.java +++ b/src/main/java/com/uploadcare/data/FileData.java @@ -24,6 +24,9 @@ public class FileData { public File.VideoInfo videoInfo; public Map rekognitionInfo; public Map variations; + public File.ContentInfo contentInfo; + public Map metadata; + public Map appdata; @Override public String toString() { diff --git a/src/test/java/com/uploadcare/api/FileTest.java b/src/test/java/com/uploadcare/api/FileTest.java index 747d424..7390226 100644 --- a/src/test/java/com/uploadcare/api/FileTest.java +++ b/src/test/java/com/uploadcare/api/FileTest.java @@ -3,26 +3,172 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.uploadcare.data.FileData; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.*; + public class FileTest { - @Test - public void enumFails() throws Exception { - String json = "{ \"color_mode\": \"RGBa\"}"; + private ObjectMapper mapper; - // duplicate the way the mapper is configured in uploadcare - ObjectMapper mapper = new ObjectMapper(); + @Before + public void setUp() { + mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + + @Test + public void enumFails() throws Exception { + String json = "{ \"color_mode\": \"RGBa\"}"; Bug bug = mapper.readValue(json, Bug.class); Assert.assertTrue("Color mode was not properly converted!", File.ColorMode.RGBa.equals(bug.colorMode)); } + @Test + public void testContentInfoDeserialization() throws Exception { + String json = "{" + + "\"uuid\": \"22240276-2f06-41f8-9411-755c8ce926ed\"," + + "\"is_image\": true," + + "\"is_ready\": true," + + "\"mime_type\": \"image/jpeg\"," + + "\"size\": 642," + + "\"content_info\": {" + + " \"mime\": {" + + " \"mime\": \"image/jpeg\"," + + " \"type\": \"image\"," + + " \"subtype\": \"jpeg\"" + + " }," + + " \"image\": {" + + " \"format\": \"JPEG\"," + + " \"width\": 500," + + " \"height\": 500," + + " \"sequence\": false," + + " \"color_mode\": \"RGB\"," + + " \"orientation\": 6" + + " }" + + "}" + + "}"; + + FileData fileData = mapper.readValue(json, FileData.class); + + assertNotNull("contentInfo should not be null", fileData.contentInfo); + assertNotNull("contentInfo.mime should not be null", fileData.contentInfo.mime); + assertEquals("image/jpeg", fileData.contentInfo.mime.mime); + assertEquals("image", fileData.contentInfo.mime.type); + assertEquals("jpeg", fileData.contentInfo.mime.subtype); + + assertNotNull("contentInfo.image should not be null", fileData.contentInfo.image); + assertEquals("JPEG", fileData.contentInfo.image.format); + assertEquals(500, fileData.contentInfo.image.width); + assertEquals(500, fileData.contentInfo.image.height); + assertEquals(File.ColorMode.RGB, fileData.contentInfo.image.colorMode); + } + + @Test + public void testVideoContentInfoDeserialization() throws Exception { + String json = "{" + + "\"uuid\": \"abc123\"," + + "\"is_image\": false," + + "\"is_ready\": true," + + "\"mime_type\": \"video/mp4\"," + + "\"size\": 1048576," + + "\"content_info\": {" + + " \"mime\": {" + + " \"mime\": \"video/mp4\"," + + " \"type\": \"video\"," + + " \"subtype\": \"mp4\"" + + " }," + + " \"video\": {" + + " \"format\": \"MP4\"," + + " \"duration\": 12000," + + " \"bitrate\": 1500000," + + " \"video\": {" + + " \"height\": 1080," + + " \"width\": 1920," + + " \"frame_rate\": 25.0," + + " \"bitrate\": 1400000," + + " \"codec\": \"h264\"" + + " }," + + " \"audio\": {" + + " \"bitrate\": 128000," + + " \"codec\": \"aac\"," + + " \"channels\": \"2\"," + + " \"sample_rate\": 44100" + + " }" + + " }" + + "}" + + "}"; + + FileData fileData = mapper.readValue(json, FileData.class); + + assertNotNull("contentInfo should not be null", fileData.contentInfo); + assertNotNull("contentInfo.mime should not be null", fileData.contentInfo.mime); + assertEquals("video/mp4", fileData.contentInfo.mime.mime); + assertEquals("video", fileData.contentInfo.mime.type); + + assertNotNull("contentInfo.video should not be null", fileData.contentInfo.video); + assertEquals("MP4", fileData.contentInfo.video.format); + assertEquals(12000, fileData.contentInfo.video.duration); + assertNotNull("video.video should not be null", fileData.contentInfo.video.video); + assertEquals(1920, fileData.contentInfo.video.video.width); + assertEquals(1080, fileData.contentInfo.video.video.height); + assertNotNull("video.audio should not be null", fileData.contentInfo.video.audio); + assertEquals("aac", fileData.contentInfo.video.audio.codec); + } + + @Test + public void testMetadataDeserialization() throws Exception { + String json = "{" + + "\"uuid\": \"22240276-2f06-41f8-9411-755c8ce926ed\"," + + "\"metadata\": {" + + " \"subsystem\": \"uploader\"," + + " \"pet\": \"cat\"" + + "}" + + "}"; + + FileData fileData = mapper.readValue(json, FileData.class); + + assertNotNull("metadata should not be null", fileData.metadata); + assertEquals("uploader", fileData.metadata.get("subsystem")); + assertEquals("cat", fileData.metadata.get("pet")); + } + + @Test + public void testAppDataDeserialization() throws Exception { + String json = "{" + + "\"uuid\": \"22240276-2f06-41f8-9411-755c8ce926ed\"," + + "\"appdata\": {" + + " \"uc_clamav_virus_scan\": {" + + " \"data\": {" + + " \"infected\": false," + + " \"infected_with\": null" + + " }," + + " \"version\": \"0.104.2\"," + + " \"datetime_created\": \"2021-09-21T11:24:33\"," + + " \"datetime_updated\": \"2021-09-21T11:24:33\"" + + " }" + + "}" + + "}"; + + FileData fileData = mapper.readValue(json, FileData.class); + + assertNotNull("appdata should not be null", fileData.appdata); + assertTrue("appdata should contain uc_clamav_virus_scan", fileData.appdata.containsKey("uc_clamav_virus_scan")); + + File.AppData appData = fileData.appdata.get("uc_clamav_virus_scan"); + + assertNotNull("appdata entry should not be null", appData); + assertEquals("0.104.2", appData.version); + assertNotNull("appdata.data should not be null", appData.data); + } + static class Bug { public File.ColorMode colorMode; } diff --git a/src/test/java/com/uploadcare/api/RestApiVersionITTest.java b/src/test/java/com/uploadcare/api/RestApiVersionITTest.java new file mode 100644 index 0000000..17fe60e --- /dev/null +++ b/src/test/java/com/uploadcare/api/RestApiVersionITTest.java @@ -0,0 +1,202 @@ +package com.uploadcare.api; + +import com.uploadcare.upload.FileUploader; +import com.uploadcare.urls.Urls; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.io.InputStream; +import java.net.URI; +import java.util.Arrays; +import java.util.Date; + +import static org.junit.Assert.*; + +/** + * Integration tests for REST API v0.7 support. + * + *

These tests connect to the live Uploadcare API and require real credentials provided via + * environment variables: + *

    + *
  • {@code UPLOADCARE_PUBLIC_KEY} – The Uploadcare project public key
  • + *
  • {@code UPLOADCARE_SECRET_KEY} – The Uploadcare project secret key
  • + *
+ * + *

Run with: + *

+ *   UPLOADCARE_PUBLIC_KEY=<pub> UPLOADCARE_SECRET_KEY=<sec> ./gradlew integrationTest
+ * 
+ * + *

When the environment variables are absent, the tests are automatically skipped. + */ +public class RestApiVersionITTest +{ + + private static final String PUBLIC_KEY = System.getenv("UPLOADCARE_PUBLIC_KEY"); + + private static final String SECRET_KEY = System.getenv("UPLOADCARE_SECRET_KEY"); + + + @Before + public void requireCredentials() { + Assume.assumeTrue("UPLOADCARE_PUBLIC_KEY environment variable must be set to run integration tests", + PUBLIC_KEY != null && !PUBLIC_KEY.isEmpty()); + Assume.assumeTrue("UPLOADCARE_SECRET_KEY environment variable must be set to run integration tests", + SECRET_KEY != null && !SECRET_KEY.isEmpty()); + } + + // ------------------------------------------------------------------------- + // Bug reproduction + // ------------------------------------------------------------------------- + + /** + * Reproduces the bug: using {@code Accept: application/vnd.uploadcare-v0.6+json} causes the + * Uploadcare API to include a {@code Warning} response header asking clients to migrate to + * the latest version. + * + *

Before the fix the Java client sent the v0.6 Accept header on every request, which + * silently received this deprecation warning without surfacing it to the caller. + * + *

Uses the {@code /files/} endpoint which is not itself deprecated, so the Warning header + * is only present when the API version is outdated (not due to endpoint deprecation). + */ + @Test + public void test_v06AcceptHeader_triggersDeprecationWarning() throws Exception { + CloseableHttpClient httpClient = HttpClients.createDefault(); + try { + HttpGet request = buildRequest(Urls.apiFiles(), "application/vnd.uploadcare-v0.6+json"); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + Header warningHeader = response.getFirstHeader("Warning"); + assertNotNull( + "BUG REPRODUCTION: Expected a Warning response header when using the " + + "deprecated v0.6 Accept header, but none was found.\n" + + "All response headers received: " + + Arrays.toString(response.getAllHeaders()), + warningHeader); + assertTrue( + "BUG REPRODUCTION: The Warning header should mention API version deprecation " + + "(e.g. 'not 0.6'), but was: " + warningHeader.getValue(), + warningHeader.getValue().contains("0.6")); + } + } finally { + httpClient.close(); + } + } + + // ------------------------------------------------------------------------- + // Fix verification – header level + // ------------------------------------------------------------------------- + + /** + * Verifies the fix at the HTTP layer: using {@code Accept: application/vnd.uploadcare-v0.7+json} + * produces no API-version deprecation {@code Warning} response header. + * + *

Uses the {@code /files/} endpoint (not the deprecated {@code /project/} endpoint) so + * that any Warning header present is only about the API version, not endpoint deprecation. + * With v0.7, the API returns at most a generic notice (e.g. demo-project), but never a + * warning that says "not 0.6" or asks the client to upgrade. + */ + @Test + public void test_v07AcceptHeader_noDeprecationWarning() throws Exception { + CloseableHttpClient httpClient = HttpClients.createDefault(); + try { + HttpGet request = buildRequest(Urls.apiFiles(), "application/vnd.uploadcare-v0.7+json"); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + Header warningHeader = response.getFirstHeader("Warning"); + // A Warning header is acceptable as long as it is NOT an API-version deprecation + // warning (e.g. demo-project notices are fine, but "not 0.6" / "API version" + // upgrade messages must not appear when the client already uses v0.7). + if (warningHeader != null) { + assertFalse("FIX VERIFICATION: No API-version deprecation Warning should be returned " + + "when using the v0.7 Accept header, but one was found: " + + warningHeader.getValue() + "\n" + + "All response headers: " + Arrays.toString(response.getAllHeaders()), + warningHeader.getValue().contains("0.6")); + } + } + } finally { + httpClient.close(); + } + } + + // ------------------------------------------------------------------------- + // Fix verification – client + model level + // ------------------------------------------------------------------------- + + /** + * Verifies the fix end-to-end using the Uploadcare Java client: + *

    + *
  1. Uploads a real image via the Upload API.
  2. + *
  3. Retrieves the file metadata via the REST API (which now sends a v0.7 Accept header).
  4. + *
  5. Asserts that the v0.7-specific {@code content_info} field is present and correctly + * populated in the response, proving the client both sends the right version header + * and correctly deserializes the new response shape.
  6. + *
+ */ + @Test + public void test_client_uploadsFile_receivesV07ContentInfo() throws Exception { + Client client = new Client(PUBLIC_KEY, SECRET_KEY, true, null); + + InputStream imageStream = getClass().getClassLoader().getResourceAsStream("olympia.jpg"); + + assertNotNull("Test resource olympia.jpg must be on the classpath", imageStream); + + byte[] imageBytes = IOUtils.toByteArray(imageStream); + + File uploadedFile = new FileUploader(client, imageBytes, "olympia.jpg").upload(); + String fileId = uploadedFile.getFileId(); + + assertNotNull("Uploaded file must have a UUID", fileId); + + try { + // getFile() uses the REST API with the v0.7 Accept header + File file = client.getFile(fileId); + + // v0.7 response always includes content_info + File.ContentInfo contentInfo = file.getContentInfo(); + + assertNotNull("FIX VERIFICATION: content_info must be present in the v0.7 API response", contentInfo); + assertNotNull("FIX VERIFICATION: content_info.mime must be present", contentInfo.mime); + assertNotNull("FIX VERIFICATION: content_info.image must be present for an image file", contentInfo.image); + + assertEquals("FIX VERIFICATION: MIME type should be image/jpeg", "image/jpeg", contentInfo.mime.mime); + assertEquals("FIX VERIFICATION: MIME type/type should be 'image'", "image", contentInfo.mime.type); + } finally { + // Best-effort clean up: ignore errors (e.g. demo projects may not allow DELETE) + try { + client.deleteFile(fileId); + } catch (Exception ignored) { + } + } + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * Builds a GET request to the given {@code url} with the given {@code Accept} header value, + * using simple authentication (public_key:secret_key in plain text). This helper exists so + * the two header-level tests can forge any Accept header value without going through the + * main {@link RequestHelper}, whose Accept header is now hard-coded to v0.7. + */ + private HttpGet buildRequest(URI url, String acceptHeader) { + HttpGet request = new HttpGet(url); + request.setHeader("Accept", acceptHeader); + request.setHeader("Date", RequestHelper.rfc2822(new Date())); + request.setHeader("Content-Type", "application/json"); + request.setHeader("Authorization", "Uploadcare.Simple " + PUBLIC_KEY + ":" + SECRET_KEY); + + return request; + } + +}