From 700c86303aa3c6d4f5dbabc998203d62aca9be40 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 12:41:58 +0200 Subject: [PATCH] Add generated TUS protocol contract canary --- .../java/io/tus/java/client/TusClient.java | 2 +- .../java/io/tus/java/client/TusProtocol.java | 17 + .../client/GeneratedTusProtocolContract.java | 461 ++++++++++++++++++ .../TestGeneratedTusProtocolContract.java | 115 +++++ 4 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/tus/java/client/TusProtocol.java create mode 100644 src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java diff --git a/src/main/java/io/tus/java/client/TusClient.java b/src/main/java/io/tus/java/client/TusClient.java index a5cac6ad..4b56a474 100644 --- a/src/main/java/io/tus/java/client/TusClient.java +++ b/src/main/java/io/tus/java/client/TusClient.java @@ -17,7 +17,7 @@ public class TusClient { * Version of the tus protocol used by the client. The remote server needs to support this * version, too. */ - public static final String TUS_VERSION = "1.0.0"; + public static final String TUS_VERSION = TusProtocol.DEFAULT_PROTOCOL_VERSION; private URL uploadCreationURL; private Proxy proxy; diff --git a/src/main/java/io/tus/java/client/TusProtocol.java b/src/main/java/io/tus/java/client/TusProtocol.java new file mode 100644 index 00000000..b4a53891 --- /dev/null +++ b/src/main/java/io/tus/java/client/TusProtocol.java @@ -0,0 +1,17 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +/** + * Generated TUS protocol constants used by the runtime client. + */ +final class TusProtocol { + static final String DEFAULT_PROTOCOL_VERSION = "1.0.0"; + + private TusProtocol() { + } +} diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java new file mode 100644 index 00000000..a3e9b0bc --- /dev/null +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -0,0 +1,461 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +/** + * Generated TUS protocol contract fixture used by tests. + */ +final class GeneratedTusProtocolContract { + static final GeneratedTusWireVersion[] WIRE_VERSIONS = new GeneratedTusWireVersion[] { + new GeneratedTusWireVersion( + true, + "1.0.0" + ), + }; + + static final GeneratedTusProtocolOperation[] OPERATIONS = new GeneratedTusProtocolOperation[] { + new GeneratedTusProtocolOperation( + "discoverTusCapabilities", + "capability-discovery", + "OPTIONS", + "/resumable/files/", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[0] + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Extension", + "tus-extension", + true + ), + new GeneratedTusHeaderField( + "Tus-Max-Size", + "tus-max-size", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Tus-Version", + "tus-version", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "createTusUpload", + "creation", + "POST", + "/resumable/files/", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + true + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Defer-Length", + "upload-defer-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 201, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Location", + "location", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "getTusUploadOffset", + "offset-discovery", + "HEAD", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Defer-Length", + "upload-defer-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "patchTusUpload", + "upload-chunk", + "PATCH", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "binary", + "application/offset+octet-stream", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Content-Type", + "content-type", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 204, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "terminateTusUpload", + "termination", + "DELETE", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 204, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "downloadTusUpload", + "download", + "GET", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[0] + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "binary", + new GeneratedTusHeaderVariant[0] + ), + } + ), + }; + + static final GeneratedTusClientFeature[] CLIENT_FEATURES = new GeneratedTusClientFeature[] { + new GeneratedTusClientFeature( + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + } + ), + new GeneratedTusClientFeature( + "terminateUpload", + new String[] { + "terminateTusUpload", + }, + new String[] { + "retry-with-backoff", + } + ), + }; + + private GeneratedTusProtocolContract() { + } + + /** + * Generated wire-version fixture. + */ + static final class GeneratedTusWireVersion { + final boolean defaultVersion; + final String value; + + GeneratedTusWireVersion(boolean defaultVersion, String value) { + this.defaultVersion = defaultVersion; + this.value = value; + } + } + + /** + * Generated HTTP header field fixture. + */ + static final class GeneratedTusHeaderField { + final String displayName; + final String name; + final boolean required; + + GeneratedTusHeaderField(String displayName, String name, boolean required) { + this.displayName = displayName; + this.name = name; + this.required = required; + } + } + + /** + * Generated alternative HTTP header set fixture. + */ + static final class GeneratedTusHeaderVariant { + final GeneratedTusHeaderField[] fields; + + GeneratedTusHeaderVariant(GeneratedTusHeaderField[] fields) { + this.fields = fields; + } + } + + /** + * Generated request contract fixture. + */ + static final class GeneratedTusRequestContract { + final String bodyKind; + final String contentType; + final GeneratedTusHeaderVariant[] headerVariants; + + GeneratedTusRequestContract( + String bodyKind, + String contentType, + GeneratedTusHeaderVariant[] headerVariants) { + this.bodyKind = bodyKind; + this.contentType = contentType; + this.headerVariants = headerVariants; + } + } + + /** + * Generated response contract fixture. + */ + static final class GeneratedTusResponseContract { + final int statusCode; + final String bodyKind; + final GeneratedTusHeaderVariant[] headerVariants; + + GeneratedTusResponseContract( + int statusCode, + String bodyKind, + GeneratedTusHeaderVariant[] headerVariants) { + this.statusCode = statusCode; + this.bodyKind = bodyKind; + this.headerVariants = headerVariants; + } + } + + /** + * Generated protocol operation fixture. + */ + static final class GeneratedTusProtocolOperation { + final String operationId; + final String role; + final String method; + final String path; + final GeneratedTusRequestContract request; + final GeneratedTusResponseContract[] responses; + + GeneratedTusProtocolOperation( + String operationId, + String role, + String method, + String path, + GeneratedTusRequestContract request, + GeneratedTusResponseContract[] responses) { + this.operationId = operationId; + this.role = role; + this.method = method; + this.path = path; + this.request = request; + this.responses = responses; + } + } + + /** + * Generated client feature fixture. + */ + static final class GeneratedTusClientFeature { + final String featureId; + final String[] operationIds; + final String[] primitives; + + GeneratedTusClientFeature(String featureId, String[] operationIds, String[] primitives) { + this.featureId = featureId; + this.operationIds = operationIds; + this.primitives = primitives; + } + } +} diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java new file mode 100644 index 00000000..9ab98919 --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -0,0 +1,115 @@ +package io.tus.java.client; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests the generated API2 protocol contract canary. + */ +public class TestGeneratedTusProtocolContract { + + /** + * Verifies the runtime constant is sourced from the generated protocol fixture. + */ + @Test + public void testDefaultProtocolVersionMatchesRuntimeConstant() { + String generatedDefault = null; + int defaultCount = 0; + + for (GeneratedTusProtocolContract.GeneratedTusWireVersion wireVersion + : GeneratedTusProtocolContract.WIRE_VERSIONS) { + if (wireVersion.defaultVersion) { + defaultCount++; + generatedDefault = wireVersion.value; + } + } + + assertEquals(1, defaultCount); + assertEquals("1.0.0", generatedDefault); + assertEquals(generatedDefault, TusProtocol.DEFAULT_PROTOCOL_VERSION); + assertEquals(generatedDefault, TusClient.TUS_VERSION); + } + + /** + * Verifies generated request-header variants retain creation requirements. + */ + @Test + public void testCreateUploadOperationKeepsRequiredHeaders() { + GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation = + findOperation("createTusUpload"); + + assertEquals("POST", operation.method); + assertEquals("/resumable/files/", operation.path); + assertEquals(2, operation.request.headerVariants.length); + assertTrue(hasRequiredHeader(operation.request.headerVariants[0], "tus-resumable")); + assertTrue(hasRequiredHeader(operation.request.headerVariants[0], "upload-length")); + assertTrue(hasRequiredHeader(operation.request.headerVariants[1], "tus-resumable")); + assertTrue(hasRequiredHeader(operation.request.headerVariants[1], "upload-defer-length")); + } + + /** + * Verifies the generated high-level lifecycle feature points at raw protocol operations. + */ + @Test + public void testSingleUploadLifecycleFeatureReferencesProtocolOperations() { + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature("singleUploadLifecycle"); + + assertContains(feature.operationIds, "createTusUpload"); + assertContains(feature.operationIds, "getTusUploadOffset"); + assertContains(feature.operationIds, "patchTusUpload"); + assertContains(feature.primitives, "store-resume-url"); + assertContains(feature.primitives, "emit-progress"); + } + + private static GeneratedTusProtocolContract.GeneratedTusProtocolOperation findOperation( + String operationId) { + for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation + : GeneratedTusProtocolContract.OPERATIONS) { + if (operation.operationId.equals(operationId)) { + return operation; + } + } + + throw new AssertionError("Missing generated TUS operation: " + operationId); + } + + private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeature( + String featureId) { + for (GeneratedTusProtocolContract.GeneratedTusClientFeature feature + : GeneratedTusProtocolContract.CLIENT_FEATURES) { + if (feature.featureId.equals(featureId)) { + return feature; + } + } + + throw new AssertionError("Missing generated TUS client feature: " + featureId); + } + + private static boolean hasRequiredHeader( + GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant, + String headerName) { + assertNotNull(variant); + + for (GeneratedTusProtocolContract.GeneratedTusHeaderField field : variant.fields) { + if (field.required && field.name.equals(headerName)) { + return true; + } + } + + return false; + } + + private static void assertContains(String[] values, String expected) { + for (String value : values) { + if (value.equals(expected)) { + return; + } + } + + throw new AssertionError("Missing generated value: " + expected); + } +}