From 700c86303aa3c6d4f5dbabc998203d62aca9be40 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 12:41:58 +0200 Subject: [PATCH 01/24] 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); + } +} From 8ad4983b9445ffbc9616b3d3658a84f6f714cc11 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 13:10:01 +0200 Subject: [PATCH 02/24] Allow manual Java client workflow runs --- .github/workflows/lintChanges.yml | 2 ++ .github/workflows/tests.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/lintChanges.yml b/.github/workflows/lintChanges.yml index 2b7069dc..ab0b8cd9 100644 --- a/.github/workflows/lintChanges.yml +++ b/.github/workflows/lintChanges.yml @@ -1,11 +1,13 @@ name: Lint Java Code on: + workflow_dispatch: push: branches: - main pull_request: types: - opened + - ready_for_review - synchronize - unlabeled jobs: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84d25cc7..e3722daa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,12 +4,14 @@ name: Tests on: + workflow_dispatch: push: branches: - main pull_request: types: - opened + - ready_for_review - synchronize - unlabeled jobs: From 19adf55ea9341ae3e6839e04fd351bba27e5d699 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 21:18:33 +0200 Subject: [PATCH 03/24] Regenerate TUS protocol contract fixture --- .../client/GeneratedTusProtocolContract.java | 110 ++++++++++++++++++ .../TestGeneratedTusProtocolContract.java | 36 +++++- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index a3e9b0bc..fc8000f4 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -108,6 +108,49 @@ final class GeneratedTusProtocolContract { ), } ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Concat", + "upload-concat", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + false + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Concat", + "upload-concat", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + false + ), + } + ), } ), new GeneratedTusResponseContract[] { @@ -328,12 +371,79 @@ final class GeneratedTusProtocolContract { "abort-current-request", } ), + new GeneratedTusClientFeature( + "resumeUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + } + ), + new GeneratedTusClientFeature( + "deferredLengthUpload", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + "creationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + } + ), + new GeneratedTusClientFeature( + "parallelUploadConcat", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + "retryOffsetRecovery", + new String[] { + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "recover-offset-after-error", + } + ), new GeneratedTusClientFeature( "terminateUpload", new String[] { "terminateTusUpload", }, new String[] { + "terminate-upload", "retry-with-backoff", } ), diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index 9ab98919..5c5a5cd2 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -43,11 +43,17 @@ public void testCreateUploadOperationKeepsRequiredHeaders() { 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")); + assertRequiredHeaderVariant( + operation.request.headerVariants, "tus-resumable", "upload-length"); + assertRequiredHeaderVariant( + operation.request.headerVariants, "tus-resumable", "upload-defer-length"); + assertRequiredHeaderVariant( + operation.request.headerVariants, + "tus-resumable", + "upload-concat", + "upload-length"); + assertRequiredHeaderVariant( + operation.request.headerVariants, "tus-resumable", "upload-concat"); } /** @@ -103,6 +109,26 @@ private static boolean hasRequiredHeader( return false; } + private static void assertRequiredHeaderVariant( + GeneratedTusProtocolContract.GeneratedTusHeaderVariant[] variants, + String... headerNames) { + for (GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant : variants) { + boolean hasAllHeaders = true; + for (String headerName : headerNames) { + if (!hasRequiredHeader(variant, headerName)) { + hasAllHeaders = false; + break; + } + } + + if (hasAllHeaders) { + return; + } + } + + throw new AssertionError("Missing generated header variant"); + } + private static void assertContains(String[] values, String expected) { for (String value : values) { if (value.equals(expected)) { From 194b752ebbf7334b663e13afbc37fa943faff4c3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 21:22:43 +0200 Subject: [PATCH 04/24] Fix generated contract lint --- .../io/tus/java/client/TestGeneratedTusProtocolContract.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index 5c5a5cd2..6d57686e 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -4,7 +4,6 @@ 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. From 9d1277f83c652b7dfbcc0fb87149a83ffa20d52a Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 22:12:27 +0200 Subject: [PATCH 05/24] Regenerate TUS feature contract fixture --- .../client/GeneratedTusProtocolContract.java | 566 +++++++++++++++++- 1 file changed, 565 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index fc8000f4..567d7385 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -356,7 +356,37 @@ final class GeneratedTusProtocolContract { static final GeneratedTusClientFeature[] CLIENT_FEATURES = new GeneratedTusClientFeature[] { new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "singleUploadLifecycle", + }, + "covered-by-generated-scenario" + ), + "Create an upload, store its URL, upload bytes, and finish successfully.", "singleUploadLifecycle", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "open-input-source", + "", + "Open the caller input as a sliceable source." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create the remote upload resource." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes until the accepted offset reaches the known length." + ), + }, new String[] { "createTusUpload", "getTusUploadOffset", @@ -372,7 +402,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "resumeFromPreviousUpload", + }, + "covered-by-generated-scenario" + ), + "Resume a stored upload URL by discovering the remote offset before patching.", "resumeUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "resume-from-previous-upload", + "", + "Load a stored upload URL selected by fingerprint." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "getTusUploadOffset", + "", + "", + "Read the server offset for the stored upload URL." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Continue uploading from the discovered offset." + ), + }, new String[] { "getTusUploadOffset", "patchTusUpload", @@ -384,7 +444,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "deferredLengthUpload", + }, + "covered-by-generated-scenario" + ), + "Create an upload without a known length and declare the length on final PATCH.", "deferredLengthUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create the upload with deferred length." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "defer-upload-length", + "", + "Track the source until the final chunk reveals the total size." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Declare Upload-Length on the final chunk request." + ), + }, new String[] { "createTusUpload", "patchTusUpload", @@ -395,7 +485,30 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "creationWithUpload", + }, + "covered-by-generated-scenario" + ), + "Send the first bytes on the creation request when the server/client support it.", "creationWithUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create the upload while streaming the initial body." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "upload-during-creation", + "", + "Interpret the creation response as an accepted offset." + ), + }, new String[] { "createTusUpload", }, @@ -405,7 +518,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "overridePatchMethod", + }, + "covered-by-generated-scenario" + ), + "Tunnel PATCH through POST with the method-override header.", "overridePatchMethod", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "getTusUploadOffset", + "", + "", + "Resume from the upload URL before sending bytes." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "override-patch-method", + "", + "Replace PATCH with POST while preserving the protocol operation intent." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes through the overridden request." + ), + }, new String[] { "getTusUploadOffset", "patchTusUpload", @@ -415,7 +558,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "parallelUploadConcat", + }, + "covered-by-generated-scenario" + ), + "Split one input into partial uploads and concatenate their upload URLs.", "parallelUploadConcat", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "split-parallel-upload-boundaries", + "", + "Split the input into stable byte ranges." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create partial uploads for each range." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "concatenate-partial-uploads", + "", + "Create the final upload from completed partial upload URLs." + ), + }, new String[] { "createTusUpload", "patchTusUpload", @@ -423,10 +596,41 @@ final class GeneratedTusProtocolContract { new String[] { "concatenate-partial-uploads", "emit-progress", + "split-parallel-upload-boundaries", } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "retryPatchAfterOffsetRecovery", + }, + "covered-by-generated-scenario" + ), + "Recover from a failed chunk by reading the server offset before retrying.", "retryOffsetRecovery", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Attempt the chunk upload." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "recover-offset-after-error", + "", + "Discover the accepted offset after a retryable failure." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "getTusUploadOffset", + "", + "", + "Use HEAD to recover the offset before retrying PATCH." + ), + }, new String[] { "createTusUpload", "getTusUploadOffset", @@ -438,7 +642,30 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "terminateWithRetry", + }, + "covered-by-generated-scenario" + ), + "Terminate an upload resource and retry retryable termination failures.", "terminateUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "terminate-upload", + "", + "Choose server-side termination for an upload URL." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "terminateTusUpload", + "", + "", + "Delete the upload resource." + ), + }, new String[] { "terminateTusUpload", }, @@ -447,6 +674,294 @@ final class GeneratedTusProtocolContract { "retry-with-backoff", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Abort the active request, pending retry timer, and any partial uploads.", + "abortUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "abort-current-request", + "", + "Cancel in-flight transport work without emitting user callbacks after abort." + ), + }, + new String[0], + new String[] { + "abort-current-request", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Expose progress and accepted-chunk callbacks from runtime upload activity.", + "uploadCallbacks", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "emit-progress", + "", + "Report bytes sent against known or deferred length." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "emit-chunk-complete", + "", + "Report chunk size, accepted offset, and total size after server acceptance." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "emit-upload-url", + "", + "Notify once a usable upload URL is known." + ), + }, + new String[0], + new String[] { + "emit-progress", + "emit-chunk-complete", + "emit-upload-url", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Run before-request, after-response, and custom retry hooks around transport.", + "requestLifecycleHooks", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "run-request-hooks", + "", + "Call user hooks around each HTTP request/response pair." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "customize-retry", + "", + "Let user retry policy override default retry decisions." + ), + }, + new String[0], + new String[] { + "customize-retry", + "run-request-hooks", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Persist, find, resume, and optionally remove upload URLs by fingerprint.", + "resumeUrlStorage", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "fingerprint-input", + "", + "Derive a stable key for the input when possible." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "store-resume-url", + "", + "Persist upload URLs and partial-upload URLs for future resumption." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "remove-stored-url-on-success", + "", + "Remove stored upload URLs when configured after success or invalidation." + ), + }, + new String[0], + new String[] { + "fingerprint-input", + "store-resume-url", + "remove-stored-url-on-success", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Support the reference client input/source families across runtimes.", + "inputSources", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-browser-file", + "", + "Read browser Blob/File and ArrayBuffer-family inputs." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-node-stream", + "", + "Read Node streams when size and chunk constraints are satisfied." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-web-stream", + "", + "Read Web Streams with deferred or configured size." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-node-file", + "", + "Read filesystem paths and fs streams, including parallel ranges." + ), + }, + new String[0], + new String[] { + "read-browser-file", + "read-node-file", + "read-node-stream", + "read-web-stream", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Support browser and file-backed URL storage implementations.", + "urlStorageBackends", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "store-browser-url", + "", + "Persist upload records in browser localStorage." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "store-file-url", + "", + "Persist upload records in the Node file store." + ), + }, + new String[0], + new String[] { + "store-browser-url", + "store-file-url", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Select between tus v1 and supported IETF draft client protocol modes.", + "protocolVersionSelection", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "select-client-protocol", + "", + "Choose request headers and response expectations for the selected protocol." + ), + }, + new String[] { + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Normalize relative Location headers against the request endpoint.", + "relativeLocationResolution", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "resolve-relative-location", + "", + "Resolve server Location headers with the creation endpoint as origin." + ), + }, + new String[] { + "createTusUpload", + }, + new String[] { + "resolve-relative-location", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Validate option combinations before starting runtime work.", + "startOptionValidation", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "validate-start-options", + "", + "Reject missing inputs and incompatible parallel/deferred/resume options." + ), + }, + new String[0], + new String[] { + "validate-start-options", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Attach request, response, status, body, and request ID context to errors.", + "detailedErrors", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "report-detailed-errors", + "", + "Return user-facing errors with enough transport context for debugging." + ), + }, + new String[0], + new String[] { + "report-detailed-errors", + } + ), }; private GeneratedTusProtocolContract() { @@ -558,14 +1073,63 @@ static final class GeneratedTusProtocolOperation { * Generated client feature fixture. */ static final class GeneratedTusClientFeature { + final GeneratedTusClientFeatureConformance conformance; + final String description; final String featureId; + final GeneratedTusClientFeatureFlowStep[] flow; final String[] operationIds; final String[] primitives; - GeneratedTusClientFeature(String featureId, String[] operationIds, String[] primitives) { + GeneratedTusClientFeature( + GeneratedTusClientFeatureConformance conformance, + String description, + String featureId, + GeneratedTusClientFeatureFlowStep[] flow, + String[] operationIds, + String[] primitives) { + this.conformance = conformance; + this.description = description; this.featureId = featureId; + this.flow = flow; this.operationIds = operationIds; this.primitives = primitives; } } + + /** + * Generated client feature conformance coverage. + */ + static final class GeneratedTusClientFeatureConformance { + final String[] scenarioIds; + final String status; + + GeneratedTusClientFeatureConformance(String[] scenarioIds, String status) { + this.scenarioIds = scenarioIds; + this.status = status; + } + } + + /** + * Generated client feature flow step. + */ + static final class GeneratedTusClientFeatureFlowStep { + final String kind; + final String operationId; + final String primitive; + final String condition; + final String summary; + + GeneratedTusClientFeatureFlowStep( + String kind, + String operationId, + String primitive, + String condition, + String summary) { + this.kind = kind; + this.operationId = operationId; + this.primitive = primitive; + this.condition = condition; + this.summary = summary; + } + } } From 210974e28e1c8d0486e0ee84912b5bacac952ee5 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 27 May 2026 11:35:33 +0200 Subject: [PATCH 06/24] Regenerate upload body protocol fixture --- .../client/GeneratedTusProtocolContract.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 567d7385..4ac47ba2 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -517,6 +517,39 @@ final class GeneratedTusProtocolContract { "emit-progress", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "uploadBodyHeaders", + }, + "covered-by-generated-scenario" + ), + "Send protocol-specific upload body headers whenever the client transmits file bytes.", + "uploadBodyHeaders", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "send-upload-body-headers", + "", + "Attach the protocol-specific upload body content type when a request has bytes." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes with the protocol-specific body headers." + ), + }, + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + } + ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( new String[] { From 6e7a32a3aa6833131e9c40017f1ec289aab8e770 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Thu, 28 May 2026 22:39:50 +0200 Subject: [PATCH 07/24] Assert generated TUS upload events --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 4ac47ba2..00c3cc59 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -730,8 +730,12 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "singleUploadLifecycle", + "creationWithUpload", + "resumeFromPreviousUpload", + }, + "covered-by-generated-scenario" ), "Expose progress and accepted-chunk callbacks from runtime upload activity.", "uploadCallbacks", From d97c79c1f732f60a6b92c0d2866947a5d4bfcdec Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Thu, 28 May 2026 23:19:53 +0200 Subject: [PATCH 08/24] Cover TUS request lifecycle conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 00c3cc59..08a16376 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -771,8 +771,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "requestLifecycleHooks", + "retryPatchAfterOffsetRecovery", + }, + "covered-by-generated-scenario" ), "Run before-request, after-response, and custom retry hooks around transport.", "requestLifecycleHooks", From d8b4e3856d4348596be53aab528538f6a880c6c0 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:06:10 +0200 Subject: [PATCH 09/24] Cover TUS abort conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 08a16376..74302ada 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -709,8 +709,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "abortUpload", + }, + "covered-by-generated-scenario" ), "Abort the active request, pending retry timer, and any partial uploads.", "abortUpload", From a211c170de64f9820abbd0c5b7e100bdaab2cdc3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:14:09 +0200 Subject: [PATCH 10/24] Cover TUS URL storage conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 74302ada..5637c3e6 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -805,8 +805,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "singleUploadLifecycle", + "resumeFromPreviousUpload", + }, + "covered-by-generated-scenario" ), "Persist, find, resume, and optionally remove upload URLs by fingerprint.", "resumeUrlStorage", From 22e352beda1ed800c1b4d4ba3313e89af1315e2c Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:19:28 +0200 Subject: [PATCH 11/24] Cover TUS relative Location conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 5637c3e6..088580d5 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -944,8 +944,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "relativeLocationResolution", + }, + "covered-by-generated-scenario" ), "Normalize relative Location headers against the request endpoint.", "relativeLocationResolution", From 2c32dd5b5b211f5e4ea7cd524e9f19b61ffd796b Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:48:41 +0200 Subject: [PATCH 12/24] Refresh TUS input source contract --- .../tus/java/client/GeneratedTusProtocolContract.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 088580d5..1af62d52 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -845,8 +845,14 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "arrayBufferInput", + "arrayBufferViewInput", + "webReadableStreamInput", + "nodeReadableStreamInput", + "nodePathInput", + }, + "covered-by-generated-scenario" ), "Support the reference client input/source families across runtimes.", "inputSources", From db63d33c96bc53f6b8a4d7e91f061f7c5341a67d Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 18:10:38 +0200 Subject: [PATCH 13/24] Refresh TUS retry state contract --- .../client/GeneratedTusProtocolContract.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 1af62d52..9a50e54e 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -674,6 +674,41 @@ final class GeneratedTusProtocolContract { "recover-offset-after-error", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "retryPatchAfterOffsetRecovery", + }, + "covered-by-generated-scenario" + ), + "Schedule retry timers and reset retry attempts after accepted progress.", + "retryStateTransitions", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "schedule-retry-timer", + "", + "Consume the current retry delay and restart the upload after that timer fires." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "reset-retry-attempt-after-progress", + "", + "Reset retry attempts once a later retry observes server-side offset progress." + ), + }, + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "schedule-retry-timer", + "reset-retry-attempt-after-progress", + } + ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( new String[] { From 296da7cd0e0b049e47bca7b7d1489bdc5e099b03 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 20:09:07 +0200 Subject: [PATCH 14/24] Refresh TUS URL storage contract --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 9a50e54e..48580efe 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -931,8 +931,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "webStorageUrlStorageBackend", + "fileUrlStorageBackend", + }, + "covered-by-generated-scenario" ), "Support browser and file-backed URL storage implementations.", "urlStorageBackends", From c9338d9ed35c41c65e82875588812b353713524f Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 21:06:25 +0200 Subject: [PATCH 15/24] Refresh TUS protocol selection contract --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 48580efe..c33f1f1c 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -963,8 +963,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "ietfDraft05CreationWithUpload", + "ietfDraft03ResumeWithoutKnownLength", + }, + "covered-by-generated-scenario" ), "Select between tus v1 and supported IETF draft client protocol modes.", "protocolVersionSelection", From bead997e74f2906dd572b23ffdb8fe72042bfb66 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 22:26:26 +0200 Subject: [PATCH 16/24] Refresh TUS start validation contract --- .../java/client/GeneratedTusProtocolContract.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index c33f1f1c..cbcc5661 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1016,8 +1016,18 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "startValidationMissingInput", + "startValidationMissingEndpointOrUploadUrl", + "startValidationUnsupportedProtocol", + "startValidationRetryDelaysNotArray", + "startValidationParallelUploadsWithUploadUrl", + "startValidationParallelUploadsWithUploadSize", + "startValidationParallelUploadsWithDeferredLength", + "startValidationParallelBoundariesWithoutParallelUploads", + "startValidationParallelBoundariesLengthMismatch", + }, + "covered-by-generated-scenario" ), "Validate option combinations before starting runtime work.", "startOptionValidation", From 0c72eabab94c403cfc2b31522b7fff6790990601 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 23:10:16 +0200 Subject: [PATCH 17/24] Update detailed error conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index cbcc5661..4466303d 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1047,8 +1047,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "detailedCreateResponseError", + "detailedCreateRequestError", + }, + "covered-by-generated-scenario" ), "Attach request, response, status, body, and request ID context to errors.", "detailedErrors", From 84862fb8674ef6408b3cea5f1b70e31a07d92293 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 13:41:05 +0200 Subject: [PATCH 18/24] Expose generated conformance scenarios --- .../client/GeneratedTusProtocolContract.java | 715 +++++++++++++++++- .../TestGeneratedTusProtocolContract.java | 32 + 2 files changed, 745 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 4466303d..652d718a 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -488,6 +488,7 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientFeatureConformance( new String[] { "creationWithUpload", + "creationWithUploadPartialChunk", }, "covered-by-generated-scenario" ), @@ -511,6 +512,7 @@ final class GeneratedTusProtocolContract { }, new String[] { "createTusUpload", + "patchTusUpload", }, new String[] { "upload-during-creation", @@ -550,6 +552,46 @@ final class GeneratedTusProtocolContract { "send-upload-body-headers", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "customRequestHeaders", + }, + "covered-by-generated-scenario" + ), + "Apply user-provided request headers to every upload request.", + "customRequestHeaders", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "apply-custom-request-headers", + "", + "Merge user-provided headers after protocol headers are prepared." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create uploads with the configured custom headers." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes with the configured custom headers." + ), + }, + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + } + ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( new String[] { @@ -594,10 +636,11 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientFeatureConformance( new String[] { "parallelUploadConcat", + "parallelUploadAbortCleanup", }, "covered-by-generated-scenario" ), - "Split one input into partial uploads and concatenate their upload URLs.", + "Split one input into partial uploads, run the parts concurrently, clean up aborted parts, and concatenate their upload URLs.", "parallelUploadConcat", new GeneratedTusClientFeatureFlowStep[] { new GeneratedTusClientFeatureFlowStep( @@ -630,6 +673,7 @@ final class GeneratedTusProtocolContract { "concatenate-partial-uploads", "emit-progress", "split-parallel-upload-boundaries", + "terminate-upload", } ), new GeneratedTusClientFeature( @@ -746,6 +790,7 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientFeatureConformance( new String[] { "abortUpload", + "abortUploadAfterStoredUrl", }, "covered-by-generated-scenario" ), @@ -760,9 +805,12 @@ final class GeneratedTusProtocolContract { "Cancel in-flight transport work without emitting user callbacks after abort." ), }, - new String[0], + new String[] { + "terminateTusUpload", + }, new String[] { "abort-current-request", + "terminate-upload", } ), new GeneratedTusClientFeature( @@ -1024,6 +1072,7 @@ final class GeneratedTusProtocolContract { "startValidationParallelUploadsWithUploadUrl", "startValidationParallelUploadsWithUploadSize", "startValidationParallelUploadsWithDeferredLength", + "startValidationParallelUploadsWithUploadDataDuringCreation", "startValidationParallelBoundariesWithoutParallelUploads", "startValidationParallelBoundariesLengthMismatch", }, @@ -1071,6 +1120,635 @@ final class GeneratedTusProtocolContract { ), }; + static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = + new GeneratedTusClientConformanceScenario[] { + new GeneratedTusClientConformanceScenario( + "single-upload-lifecycle", + "success", + null, + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + }, + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "creation-with-upload", + "success", + null, + "creationWithUpload", + "creationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "creation-with-upload-partial-chunk", + "success", + null, + "creationWithUpload", + "creationWithUploadPartialChunk", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "creation-with-upload", + "success", + null, + "protocolVersionSelection", + "ietfDraft05CreationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "upload-body-headers", + "success", + null, + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "missingInput", + "startOptionValidation", + "startValidationMissingInput", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "missingEndpointOrUploadUrl", + "startOptionValidation", + "startValidationMissingEndpointOrUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "unsupportedProtocol", + "startOptionValidation", + "startValidationUnsupportedProtocol", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "retryDelaysNotArray", + "startOptionValidation", + "startValidationRetryDelaysNotArray", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithUploadUrl", + "startOptionValidation", + "startValidationParallelUploadsWithUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithUploadSize", + "startOptionValidation", + "startValidationParallelUploadsWithUploadSize", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithDeferredLength", + "startOptionValidation", + "startValidationParallelUploadsWithDeferredLength", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithUploadDataDuringCreation", + "startOptionValidation", + "startValidationParallelUploadsWithUploadDataDuringCreation", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelBoundariesWithoutParallelUploads", + "startOptionValidation", + "startValidationParallelBoundariesWithoutParallelUploads", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelBoundariesLengthMismatch", + "startOptionValidation", + "startValidationParallelBoundariesLengthMismatch", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "detailed-error", + "error", + "unexpectedCreateResponse", + "detailedErrors", + "detailedCreateResponseError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "detailed-error", + "error", + "createUploadRequestFailed", + "detailedErrors", + "detailedCreateRequestError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "upload-body-headers", + "success", + null, + "uploadBodyHeaders", + "uploadBodyHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "custom-request-headers", + "success", + null, + "customRequestHeaders", + "customRequestHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "resume-from-previous-upload", + "success", + null, + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + }, + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "relative-location-resolution", + "success", + null, + "relativeLocationResolution", + "relativeLocationResolution", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "resolve-relative-location", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "array-buffer-input", + "success", + null, + "inputSources", + "arrayBufferInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "array-buffer-view-input", + "success", + null, + "inputSources", + "arrayBufferViewInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "web-readable-stream-input", + "success", + null, + "inputSources", + "webReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-web-stream", + }, + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "node-readable-stream-input", + "success", + null, + "inputSources", + "nodeReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-stream", + }, + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "node-path-input", + "success", + null, + "inputSources", + "nodePathInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-file", + }, + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "deferred-length-upload", + "success", + null, + "deferredLengthUpload", + "deferredLengthUpload", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "override-patch-method", + "success", + null, + "overridePatchMethod", + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "parallel-upload-concat", + "success", + null, + "parallelUploadConcat", + "parallelUploadConcat", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "createTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + }, + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), + new GeneratedTusClientConformanceScenario( + "parallel-upload-abort-cleanup", + "aborted", + null, + "parallelUploadConcat", + "parallelUploadAbortCleanup", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + "concatenate-partial-uploads", + }, + new String[] { + "request-abort:3", + } + ), + new GeneratedTusClientConformanceScenario( + "retry-patch-after-offset-recovery", + "success", + null, + "retryOffsetRecovery", + "retryPatchAfterOffsetRecovery", + new String[] { + "createTusUpload", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "recover-offset-after-error", + }, + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ), + new GeneratedTusClientConformanceScenario( + "request-lifecycle-hooks", + "success", + null, + "requestLifecycleHooks", + "requestLifecycleHooks", + new String[] { + "getTusUploadOffset", + }, + new String[] { + "run-request-hooks", + }, + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "abort-upload", + "aborted", + null, + "abortUpload", + "abortUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "abort-current-request", + }, + new String[] { + "request-abort:0", + } + ), + new GeneratedTusClientConformanceScenario( + "abort-upload-after-stored-url", + "aborted", + null, + "abortUpload", + "abortUploadAfterStoredUrl", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + }, + new String[] { + "request-abort:1", + } + ), + new GeneratedTusClientConformanceScenario( + "terminate-with-retry", + "terminated", + null, + "terminateUpload", + "terminateWithRetry", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "terminate-upload", + "retry-with-backoff", + }, + new String[0] + ), + }; + private GeneratedTusProtocolContract() { } @@ -1239,4 +1917,37 @@ static final class GeneratedTusClientFeatureFlowStep { this.summary = summary; } } + + /** + * Generated client conformance scenario fixture. + */ + static final class GeneratedTusClientConformanceScenario { + final String behavior; + final String completionKind; + final String completionReason; + final String featureId; + final String scenarioId; + final String[] operationIds; + final String[] primitives; + final String[] eventKeys; + + GeneratedTusClientConformanceScenario( + String behavior, + String completionKind, + String completionReason, + String featureId, + String scenarioId, + String[] operationIds, + String[] primitives, + String[] eventKeys) { + this.behavior = behavior; + this.completionKind = completionKind; + this.completionReason = completionReason; + this.featureId = featureId; + this.scenarioId = scenarioId; + this.operationIds = operationIds; + this.primitives = primitives; + this.eventKeys = eventKeys; + } + } } diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index 6d57686e..ca098930 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -70,6 +70,26 @@ public void testSingleUploadLifecycleFeatureReferencesProtocolOperations() { assertContains(feature.primitives, "emit-progress"); } + /** + * Verifies generated high-level conformance scenarios expose projected event keys. + */ + @Test + public void testConformanceScenarioCarriesProjectedEventKeys() { + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature("creationWithUpload"); + GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = + findScenario("creationWithUploadPartialChunk"); + + assertContains(feature.conformance.scenarioIds, scenario.scenarioId); + assertEquals("creation-with-upload-partial-chunk", scenario.behavior); + assertEquals("success", scenario.completionKind); + assertContains(scenario.operationIds, "createTusUpload"); + assertContains(scenario.operationIds, "patchTusUpload"); + assertContains(scenario.primitives, "upload-during-creation"); + assertContains(scenario.eventKeys, "chunk-complete:5:10:11"); + assertContains(scenario.eventKeys, "chunk-complete:1:11:11"); + } + private static GeneratedTusProtocolContract.GeneratedTusProtocolOperation findOperation( String operationId) { for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation @@ -94,6 +114,18 @@ private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeatur throw new AssertionError("Missing generated TUS client feature: " + featureId); } + private static GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario findScenario( + String scenarioId) { + for (GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario + : GeneratedTusProtocolContract.CLIENT_CONFORMANCE_SCENARIOS) { + if (scenario.scenarioId.equals(scenarioId)) { + return scenario; + } + } + + throw new AssertionError("Missing generated TUS client scenario: " + scenarioId); + } + private static boolean hasRequiredHeader( GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant, String headerName) { From 9a0d85a115c22e318c166717d1155fb54c2e2bb7 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 13:55:53 +0200 Subject: [PATCH 19/24] Add generated conformance event canary --- .../TestGeneratedTusConformanceEvents.java | 285 ++++++++++++++++++ .../TestGeneratedTusProtocolContract.java | 32 -- 2 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java new file mode 100644 index 00000000..40526792 --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -0,0 +1,285 @@ +/* + * 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; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * Tests generated TUS client conformance event fixtures. + */ +public class TestGeneratedTusConformanceEvents { + private static final GeneratedTusEventCanaryCase[] CASES = + new GeneratedTusEventCanaryCase[] { + new GeneratedTusEventCanaryCase( + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "creationWithUpload", + "creationWithUpload", + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "creationWithUpload", + "creationWithUploadPartialChunk", + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "protocolVersionSelection", + "ietfDraft05CreationWithUpload", + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength", + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "relativeLocationResolution", + "relativeLocationResolution", + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "arrayBufferInput", + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "arrayBufferViewInput", + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "webReadableStreamInput", + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "nodeReadableStreamInput", + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "nodePathInput", + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "deferredLengthUpload", + "deferredLengthUpload", + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "parallelUploadConcat", + "parallelUploadConcat", + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), + new GeneratedTusEventCanaryCase( + "parallelUploadConcat", + "parallelUploadAbortCleanup", + new String[] { + "request-abort:3", + } + ), + new GeneratedTusEventCanaryCase( + "retryOffsetRecovery", + "retryPatchAfterOffsetRecovery", + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ), + new GeneratedTusEventCanaryCase( + "requestLifecycleHooks", + "requestLifecycleHooks", + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "abortUpload", + "abortUpload", + new String[] { + "request-abort:0", + } + ), + new GeneratedTusEventCanaryCase( + "abortUpload", + "abortUploadAfterStoredUrl", + new String[] { + "request-abort:1", + } + ), + }; + + /** + * Verifies generated feature-level event keys survive in the Java fixture. + */ + @Test + public void testGeneratedScenarioEventKeys() { + for (GeneratedTusEventCanaryCase testCase : CASES) { + GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = + findScenario(testCase.scenarioId); + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature(testCase.featureId); + + assertEquals(testCase.featureId, scenario.featureId); + assertContains(feature.conformance.scenarioIds, scenario.scenarioId); + assertArrayEquals(testCase.eventKeys, scenario.eventKeys); + } + } + + 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 GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario findScenario( + String scenarioId) { + for (GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario + : GeneratedTusProtocolContract.CLIENT_CONFORMANCE_SCENARIOS) { + if (scenario.scenarioId.equals(scenarioId)) { + return scenario; + } + } + + throw new AssertionError("Missing generated TUS client scenario: " + scenarioId); + } + + private static void assertContains(String[] values, String expected) { + for (String value : values) { + if (value.equals(expected)) { + return; + } + } + + throw new AssertionError("Missing generated value: " + expected); + } + + private static final class GeneratedTusEventCanaryCase { + final String featureId; + final String scenarioId; + final String[] eventKeys; + + GeneratedTusEventCanaryCase(String featureId, String scenarioId, String[] eventKeys) { + this.featureId = featureId; + this.scenarioId = scenarioId; + this.eventKeys = eventKeys; + } + } +} diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index ca098930..6d57686e 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -70,26 +70,6 @@ public void testSingleUploadLifecycleFeatureReferencesProtocolOperations() { assertContains(feature.primitives, "emit-progress"); } - /** - * Verifies generated high-level conformance scenarios expose projected event keys. - */ - @Test - public void testConformanceScenarioCarriesProjectedEventKeys() { - GeneratedTusProtocolContract.GeneratedTusClientFeature feature = - findFeature("creationWithUpload"); - GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = - findScenario("creationWithUploadPartialChunk"); - - assertContains(feature.conformance.scenarioIds, scenario.scenarioId); - assertEquals("creation-with-upload-partial-chunk", scenario.behavior); - assertEquals("success", scenario.completionKind); - assertContains(scenario.operationIds, "createTusUpload"); - assertContains(scenario.operationIds, "patchTusUpload"); - assertContains(scenario.primitives, "upload-during-creation"); - assertContains(scenario.eventKeys, "chunk-complete:5:10:11"); - assertContains(scenario.eventKeys, "chunk-complete:1:11:11"); - } - private static GeneratedTusProtocolContract.GeneratedTusProtocolOperation findOperation( String operationId) { for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation @@ -114,18 +94,6 @@ private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeatur throw new AssertionError("Missing generated TUS client feature: " + featureId); } - private static GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario findScenario( - String scenarioId) { - for (GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario - : GeneratedTusProtocolContract.CLIENT_CONFORMANCE_SCENARIOS) { - if (scenario.scenarioId.equals(scenarioId)) { - return scenario; - } - } - - throw new AssertionError("Missing generated TUS client scenario: " + scenarioId); - } - private static boolean hasRequiredHeader( GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant, String headerName) { From 6a8c8fadb98e0539b7978f9d0f150935091af576 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 19:12:52 +0200 Subject: [PATCH 20/24] Regenerate TUS protocol fixture for lint --- .../client/GeneratedTusProtocolContract.java | 230 ++++++++++++------ 1 file changed, 156 insertions(+), 74 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 652d718a..18e9d68d 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1124,8 +1124,10 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientConformanceScenario[] { new GeneratedTusClientConformanceScenario( "single-upload-lifecycle", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "singleUploadLifecycle", "singleUploadLifecycle", new String[] { @@ -1153,8 +1155,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "creation-with-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "creationWithUpload", "creationWithUpload", new String[] { @@ -1174,8 +1178,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "creation-with-upload-partial-chunk", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "creationWithUpload", "creationWithUploadPartialChunk", new String[] { @@ -1202,8 +1208,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "creation-with-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "protocolVersionSelection", "ietfDraft05CreationWithUpload", new String[] { @@ -1222,8 +1230,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "upload-body-headers", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "protocolVersionSelection", "ietfDraft03ResumeWithoutKnownLength", new String[] { @@ -1244,8 +1254,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "missingInput", + new GeneratedTusClientConformanceCompletion( + "error", + "missingInput" + ), "startOptionValidation", "startValidationMissingInput", new String[0], @@ -1256,8 +1268,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "missingEndpointOrUploadUrl", + new GeneratedTusClientConformanceCompletion( + "error", + "missingEndpointOrUploadUrl" + ), "startOptionValidation", "startValidationMissingEndpointOrUploadUrl", new String[0], @@ -1268,8 +1282,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "unsupportedProtocol", + new GeneratedTusClientConformanceCompletion( + "error", + "unsupportedProtocol" + ), "startOptionValidation", "startValidationUnsupportedProtocol", new String[0], @@ -1280,8 +1296,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "retryDelaysNotArray", + new GeneratedTusClientConformanceCompletion( + "error", + "retryDelaysNotArray" + ), "startOptionValidation", "startValidationRetryDelaysNotArray", new String[0], @@ -1292,8 +1310,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithUploadUrl", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadUrl" + ), "startOptionValidation", "startValidationParallelUploadsWithUploadUrl", new String[0], @@ -1304,8 +1324,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithUploadSize", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadSize" + ), "startOptionValidation", "startValidationParallelUploadsWithUploadSize", new String[0], @@ -1316,8 +1338,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithDeferredLength", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithDeferredLength" + ), "startOptionValidation", "startValidationParallelUploadsWithDeferredLength", new String[0], @@ -1328,8 +1352,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithUploadDataDuringCreation", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadDataDuringCreation" + ), "startOptionValidation", "startValidationParallelUploadsWithUploadDataDuringCreation", new String[0], @@ -1340,8 +1366,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelBoundariesWithoutParallelUploads", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesWithoutParallelUploads" + ), "startOptionValidation", "startValidationParallelBoundariesWithoutParallelUploads", new String[0], @@ -1352,8 +1380,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelBoundariesLengthMismatch", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesLengthMismatch" + ), "startOptionValidation", "startValidationParallelBoundariesLengthMismatch", new String[0], @@ -1364,8 +1394,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "detailed-error", - "error", - "unexpectedCreateResponse", + new GeneratedTusClientConformanceCompletion( + "error", + "unexpectedCreateResponse" + ), "detailedErrors", "detailedCreateResponseError", new String[] { @@ -1378,8 +1410,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "detailed-error", - "error", - "createUploadRequestFailed", + new GeneratedTusClientConformanceCompletion( + "error", + "createUploadRequestFailed" + ), "detailedErrors", "detailedCreateRequestError", new String[] { @@ -1392,8 +1426,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "upload-body-headers", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "uploadBodyHeaders", "uploadBodyHeaders", new String[] { @@ -1407,8 +1443,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "custom-request-headers", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "customRequestHeaders", "customRequestHeaders", new String[] { @@ -1422,8 +1460,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "resume-from-previous-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "resumeUpload", "resumeFromPreviousUpload", new String[] { @@ -1450,8 +1490,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "relative-location-resolution", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "relativeLocationResolution", "relativeLocationResolution", new String[] { @@ -1472,8 +1514,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "array-buffer-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "arrayBufferInput", new String[] { @@ -1491,8 +1535,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "array-buffer-view-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "arrayBufferViewInput", new String[] { @@ -1510,8 +1556,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "web-readable-stream-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "webReadableStreamInput", new String[] { @@ -1529,8 +1577,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "node-readable-stream-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "nodeReadableStreamInput", new String[] { @@ -1548,8 +1598,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "node-path-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "nodePathInput", new String[] { @@ -1567,8 +1619,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "deferred-length-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "deferredLengthUpload", "deferredLengthUpload", new String[] { @@ -1590,8 +1644,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "override-patch-method", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "overridePatchMethod", "overridePatchMethod", new String[] { @@ -1605,8 +1661,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "parallel-upload-concat", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "parallelUploadConcat", "parallelUploadConcat", new String[] { @@ -1629,8 +1687,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "parallel-upload-abort-cleanup", - "aborted", - null, + new GeneratedTusClientConformanceCompletion( + "aborted", + null + ), "parallelUploadConcat", "parallelUploadAbortCleanup", new String[] { @@ -1652,8 +1712,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "retry-patch-after-offset-recovery", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "retryOffsetRecovery", "retryPatchAfterOffsetRecovery", new String[] { @@ -1677,8 +1739,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "request-lifecycle-hooks", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "requestLifecycleHooks", "requestLifecycleHooks", new String[] { @@ -1696,8 +1760,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "abort-upload", - "aborted", - null, + new GeneratedTusClientConformanceCompletion( + "aborted", + null + ), "abortUpload", "abortUpload", new String[] { @@ -1712,8 +1778,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "abort-upload-after-stored-url", - "aborted", - null, + new GeneratedTusClientConformanceCompletion( + "aborted", + null + ), "abortUpload", "abortUploadAfterStoredUrl", new String[] { @@ -1731,8 +1799,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "terminate-with-retry", - "terminated", - null, + new GeneratedTusClientConformanceCompletion( + "terminated", + null + ), "terminateUpload", "terminateWithRetry", new String[] { @@ -1933,16 +2003,15 @@ static final class GeneratedTusClientConformanceScenario { GeneratedTusClientConformanceScenario( String behavior, - String completionKind, - String completionReason, + GeneratedTusClientConformanceCompletion completion, String featureId, String scenarioId, String[] operationIds, String[] primitives, String[] eventKeys) { this.behavior = behavior; - this.completionKind = completionKind; - this.completionReason = completionReason; + this.completionKind = completion.kind; + this.completionReason = completion.reason; this.featureId = featureId; this.scenarioId = scenarioId; this.operationIds = operationIds; @@ -1950,4 +2019,17 @@ static final class GeneratedTusClientConformanceScenario { this.eventKeys = eventKeys; } } + + /** + * Generated client conformance completion fixture. + */ + static final class GeneratedTusClientConformanceCompletion { + final String kind; + final String reason; + + GeneratedTusClientConformanceCompletion(String kind, String reason) { + this.kind = kind; + this.reason = reason; + } + } } From 1a3a0852e12c527024b5b72859dd9593f92961cb Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 20:17:51 +0200 Subject: [PATCH 21/24] Add generated runtime event canary --- .../java/io/tus/java/client/TusUploader.java | 72 ++ ...eneratedTusClientConformanceScenarios.java | 714 ++++++++++++++++++ .../client/GeneratedTusProtocolContract.java | 698 +---------------- .../client/TestGeneratedTusRuntimeEvents.java | 315 ++++++++ 4 files changed, 1102 insertions(+), 697 deletions(-) create mode 100644 src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java diff --git a/src/main/java/io/tus/java/client/TusUploader.java b/src/main/java/io/tus/java/client/TusUploader.java index d84bd8b4..8af4c462 100644 --- a/src/main/java/io/tus/java/client/TusUploader.java +++ b/src/main/java/io/tus/java/client/TusUploader.java @@ -21,6 +21,31 @@ * */ public class TusUploader { + /** + * Callback for upload progress events. + */ + public interface ProgressListener { + /** + * Called when upload progress changes. + * @param bytesSent Bytes accepted locally for the upload. + * @param bytesTotal Total upload size. + */ + void onProgress(long bytesSent, long bytesTotal); + } + + /** + * Callback for accepted chunk events. + */ + public interface ChunkCompleteListener { + /** + * Called after the server accepts an upload request. + * @param chunkSize Bytes accepted by the completed request. + * @param bytesAccepted Total bytes accepted by the server. + * @param bytesTotal Total upload size. + */ + void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal); + } + private URL uploadURL; private Proxy proxy; private TusInputStream input; @@ -30,6 +55,10 @@ public class TusUploader { private byte[] buffer; private int requestPayloadSize = 10 * 1024 * 1024; private int bytesRemainingForRequest; + private long requestStartOffset; + private boolean requestProgressStarted; + private ProgressListener progressListener; + private ChunkCompleteListener chunkCompleteListener; private HttpURLConnection connection; private OutputStream output; @@ -65,6 +94,8 @@ private void openConnection() throws IOException, ProtocolException { } bytesRemainingForRequest = requestPayloadSize; + requestStartOffset = offset; + requestProgressStarted = false; input.mark(requestPayloadSize); if (proxy != null) { @@ -168,6 +199,24 @@ public int getRequestPayloadSize() { return requestPayloadSize; } + /** + * Set the listener used for upload progress events. + * + * @param listener Progress listener or null to disable events. + */ + public void setProgressListener(ProgressListener listener) { + progressListener = listener; + } + + /** + * Set the listener used for accepted chunk events. + * + * @param listener Chunk-complete listener or null to disable events. + */ + public void setChunkCompleteListener(ChunkCompleteListener listener) { + chunkCompleteListener = listener; + } + /** * Upload a part of the file by reading a chunk from the InputStream and writing * it to the HTTP request's body. If the number of available bytes is lower than the chunk's @@ -184,6 +233,7 @@ public int getRequestPayloadSize() { */ public int uploadChunk() throws IOException, ProtocolException { openConnection(); + notifyProgressAtRequestStart(); int bytesToRead = Math.min(getChunkSize(), bytesRemainingForRequest); @@ -201,6 +251,7 @@ public int uploadChunk() throws IOException, ProtocolException { offset += bytesRead; bytesRemainingForRequest -= bytesRead; + notifyProgress(offset); if (bytesRemainingForRequest <= 0) { finishConnection(); @@ -358,7 +409,28 @@ private void finishConnection() throws ProtocolException, IOException { connection); } + notifyChunkComplete(serverOffset - requestStartOffset, serverOffset); connection = null; + requestProgressStarted = false; + } + } + + private void notifyProgressAtRequestStart() { + if (!requestProgressStarted) { + notifyProgress(offset); + requestProgressStarted = true; + } + } + + private void notifyProgress(long bytesSent) { + if (progressListener != null) { + progressListener.onProgress(bytesSent, upload.getSize()); + } + } + + private void notifyChunkComplete(long chunkSize, long bytesAccepted) { + if (chunkCompleteListener != null) { + chunkCompleteListener.onChunkComplete(chunkSize, bytesAccepted, upload.getSize()); } } diff --git a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java new file mode 100644 index 00000000..5775ae46 --- /dev/null +++ b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java @@ -0,0 +1,714 @@ +/* + * 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 client conformance scenario fixture used by tests. + */ +final class GeneratedTusClientConformanceScenarios { + static final GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario[] { + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "single-upload-lifecycle", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + }, + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "creation-with-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "creationWithUpload", + "creationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "creation-with-upload-partial-chunk", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "creationWithUpload", + "creationWithUploadPartialChunk", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "creation-with-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "protocolVersionSelection", + "ietfDraft05CreationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "upload-body-headers", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "missingInput" + ), + "startOptionValidation", + "startValidationMissingInput", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "missingEndpointOrUploadUrl" + ), + "startOptionValidation", + "startValidationMissingEndpointOrUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "unsupportedProtocol" + ), + "startOptionValidation", + "startValidationUnsupportedProtocol", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "retryDelaysNotArray" + ), + "startOptionValidation", + "startValidationRetryDelaysNotArray", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadUrl" + ), + "startOptionValidation", + "startValidationParallelUploadsWithUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadSize" + ), + "startOptionValidation", + "startValidationParallelUploadsWithUploadSize", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithDeferredLength" + ), + "startOptionValidation", + "startValidationParallelUploadsWithDeferredLength", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadDataDuringCreation" + ), + "startOptionValidation", + "startValidationParallelUploadsWithUploadDataDuringCreation", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesWithoutParallelUploads" + ), + "startOptionValidation", + "startValidationParallelBoundariesWithoutParallelUploads", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesLengthMismatch" + ), + "startOptionValidation", + "startValidationParallelBoundariesLengthMismatch", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "detailed-error", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "unexpectedCreateResponse" + ), + "detailedErrors", + "detailedCreateResponseError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "detailed-error", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "createUploadRequestFailed" + ), + "detailedErrors", + "detailedCreateRequestError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "upload-body-headers", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "uploadBodyHeaders", + "uploadBodyHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "custom-request-headers", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "customRequestHeaders", + "customRequestHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "resume-from-previous-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + }, + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "relative-location-resolution", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "relativeLocationResolution", + "relativeLocationResolution", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "resolve-relative-location", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "array-buffer-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "arrayBufferInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "array-buffer-view-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "arrayBufferViewInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "web-readable-stream-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "webReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-web-stream", + }, + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "node-readable-stream-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "nodeReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-stream", + }, + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "node-path-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "nodePathInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-file", + }, + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "deferred-length-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "deferredLengthUpload", + "deferredLengthUpload", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "override-patch-method", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "overridePatchMethod", + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "parallel-upload-concat", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "parallelUploadConcat", + "parallelUploadConcat", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "createTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + }, + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "parallel-upload-abort-cleanup", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "aborted", + null + ), + "parallelUploadConcat", + "parallelUploadAbortCleanup", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + "concatenate-partial-uploads", + }, + new String[] { + "request-abort:3", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "retry-patch-after-offset-recovery", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "retryOffsetRecovery", + "retryPatchAfterOffsetRecovery", + new String[] { + "createTusUpload", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "recover-offset-after-error", + }, + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "request-lifecycle-hooks", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "requestLifecycleHooks", + "requestLifecycleHooks", + new String[] { + "getTusUploadOffset", + }, + new String[] { + "run-request-hooks", + }, + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "abort-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "aborted", + null + ), + "abortUpload", + "abortUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "abort-current-request", + }, + new String[] { + "request-abort:0", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "abort-upload-after-stored-url", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "aborted", + null + ), + "abortUpload", + "abortUploadAfterStoredUrl", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + }, + new String[] { + "request-abort:1", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "terminate-with-retry", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "terminated", + null + ), + "terminateUpload", + "terminateWithRetry", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "terminate-upload", + "retry-with-backoff", + }, + new String[0] + ), + }; + + private GeneratedTusClientConformanceScenarios() { + } +} diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 18e9d68d..e4108fbf 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,703 +1121,7 @@ final class GeneratedTusProtocolContract { }; static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = - new GeneratedTusClientConformanceScenario[] { - new GeneratedTusClientConformanceScenario( - "single-upload-lifecycle", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "singleUploadLifecycle", - "singleUploadLifecycle", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "open-input-source", - "fingerprint-input", - "store-resume-url", - "retry-with-backoff", - "emit-progress", - "abort-current-request", - }, - new String[] { - "fingerprint:contract-single-fingerprint", - "upload-url-available", - "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "creation-with-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "creationWithUpload", - "creationWithUpload", - new String[] { - "createTusUpload", - }, - new String[] { - "upload-during-creation", - "emit-progress", - }, - new String[] { - "progress:0:11", - "progress:11:11", - "upload-url-available", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "creation-with-upload-partial-chunk", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "creationWithUpload", - "creationWithUploadPartialChunk", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "upload-during-creation", - "emit-progress", - }, - new String[] { - "progress:0:11", - "progress:5:11", - "upload-url-available", - "progress:5:11", - "progress:10:11", - "chunk-complete:5:10:11", - "progress:10:11", - "progress:11:11", - "chunk-complete:1:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "creation-with-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "protocolVersionSelection", - "ietfDraft05CreationWithUpload", - new String[] { - "createTusUpload", - }, - new String[] { - "select-client-protocol", - }, - new String[] { - "progress:0:11", - "progress:11:11", - "upload-url-available", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "upload-body-headers", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "protocolVersionSelection", - "ietfDraft03ResumeWithoutKnownLength", - new String[] { - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "select-client-protocol", - }, - new String[] { - "upload-url-available", - "progress:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "missingInput" - ), - "startOptionValidation", - "startValidationMissingInput", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "missingEndpointOrUploadUrl" - ), - "startOptionValidation", - "startValidationMissingEndpointOrUploadUrl", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "unsupportedProtocol" - ), - "startOptionValidation", - "startValidationUnsupportedProtocol", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "retryDelaysNotArray" - ), - "startOptionValidation", - "startValidationRetryDelaysNotArray", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithUploadUrl" - ), - "startOptionValidation", - "startValidationParallelUploadsWithUploadUrl", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithUploadSize" - ), - "startOptionValidation", - "startValidationParallelUploadsWithUploadSize", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithDeferredLength" - ), - "startOptionValidation", - "startValidationParallelUploadsWithDeferredLength", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithUploadDataDuringCreation" - ), - "startOptionValidation", - "startValidationParallelUploadsWithUploadDataDuringCreation", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelBoundariesWithoutParallelUploads" - ), - "startOptionValidation", - "startValidationParallelBoundariesWithoutParallelUploads", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelBoundariesLengthMismatch" - ), - "startOptionValidation", - "startValidationParallelBoundariesLengthMismatch", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "detailed-error", - new GeneratedTusClientConformanceCompletion( - "error", - "unexpectedCreateResponse" - ), - "detailedErrors", - "detailedCreateResponseError", - new String[] { - "createTusUpload", - }, - new String[] { - "report-detailed-errors", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "detailed-error", - new GeneratedTusClientConformanceCompletion( - "error", - "createUploadRequestFailed" - ), - "detailedErrors", - "detailedCreateRequestError", - new String[] { - "createTusUpload", - }, - new String[] { - "report-detailed-errors", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "upload-body-headers", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "uploadBodyHeaders", - "uploadBodyHeaders", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "send-upload-body-headers", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "custom-request-headers", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "customRequestHeaders", - "customRequestHeaders", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "apply-custom-request-headers", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "resume-from-previous-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "resumeUpload", - "resumeFromPreviousUpload", - new String[] { - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "fingerprint-input", - "resume-from-previous-upload", - "store-resume-url", - }, - new String[] { - "fingerprint:contract-resume-fingerprint", - "url-storage-find:contract-resume-fingerprint:1", - "fingerprint:contract-resume-fingerprint", - "upload-url-available", - "progress:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - "url-storage-remove:tus::contract-resume-fingerprint::1337", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "relative-location-resolution", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "relativeLocationResolution", - "relativeLocationResolution", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "resolve-relative-location", - }, - new String[] { - "upload-url-available", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "array-buffer-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "arrayBufferInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-browser-file", - }, - new String[] { - "source-open:array-buffer:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "array-buffer-view-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "arrayBufferViewInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-browser-file", - }, - new String[] { - "source-open:array-buffer-view:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "web-readable-stream-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "webReadableStreamInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-web-stream", - }, - new String[] { - "source-open:web-readable-stream:null", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "node-readable-stream-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "nodeReadableStreamInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-node-stream", - }, - new String[] { - "source-open:node-readable-stream:null", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "node-path-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "nodePathInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-node-file", - }, - new String[] { - "source-open:node-path-reference:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "deferred-length-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "deferredLengthUpload", - "deferredLengthUpload", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "defer-upload-length", - "emit-progress", - }, - new String[] { - "upload-url-available", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "override-patch-method", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "overridePatchMethod", - "overridePatchMethod", - new String[] { - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "override-patch-method", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "parallel-upload-concat", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "parallelUploadConcat", - "parallelUploadConcat", - new String[] { - "createTusUpload", - "createTusUpload", - "patchTusUpload", - "patchTusUpload", - "createTusUpload", - }, - new String[] { - "concatenate-partial-uploads", - "emit-progress", - }, - new String[] { - "progress:5:11", - "chunk-complete:5:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - } - ), - new GeneratedTusClientConformanceScenario( - "parallel-upload-abort-cleanup", - new GeneratedTusClientConformanceCompletion( - "aborted", - null - ), - "parallelUploadConcat", - "parallelUploadAbortCleanup", - new String[] { - "createTusUpload", - "createTusUpload", - "patchTusUpload", - "patchTusUpload", - "terminateTusUpload", - "terminateTusUpload", - }, - new String[] { - "abort-current-request", - "terminate-upload", - "concatenate-partial-uploads", - }, - new String[] { - "request-abort:3", - } - ), - new GeneratedTusClientConformanceScenario( - "retry-patch-after-offset-recovery", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "retryOffsetRecovery", - "retryPatchAfterOffsetRecovery", - new String[] { - "createTusUpload", - "patchTusUpload", - "getTusUploadOffset", - "patchTusUpload", - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "retry-with-backoff", - "recover-offset-after-error", - }, - new String[] { - "should-retry:0:true", - "retry-schedule:0", - "should-retry:0:true", - "retry-schedule:0", - } - ), - new GeneratedTusClientConformanceScenario( - "request-lifecycle-hooks", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "requestLifecycleHooks", - "requestLifecycleHooks", - new String[] { - "getTusUploadOffset", - }, - new String[] { - "run-request-hooks", - }, - new String[] { - "before-request:0", - "after-response:0", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "abort-upload", - new GeneratedTusClientConformanceCompletion( - "aborted", - null - ), - "abortUpload", - "abortUpload", - new String[] { - "createTusUpload", - }, - new String[] { - "abort-current-request", - }, - new String[] { - "request-abort:0", - } - ), - new GeneratedTusClientConformanceScenario( - "abort-upload-after-stored-url", - new GeneratedTusClientConformanceCompletion( - "aborted", - null - ), - "abortUpload", - "abortUploadAfterStoredUrl", - new String[] { - "createTusUpload", - "patchTusUpload", - "terminateTusUpload", - }, - new String[] { - "abort-current-request", - "terminate-upload", - }, - new String[] { - "request-abort:1", - } - ), - new GeneratedTusClientConformanceScenario( - "terminate-with-retry", - new GeneratedTusClientConformanceCompletion( - "terminated", - null - ), - "terminateUpload", - "terminateWithRetry", - new String[] { - "createTusUpload", - "patchTusUpload", - "terminateTusUpload", - "terminateTusUpload", - }, - new String[] { - "terminate-upload", - "retry-with-backoff", - }, - new String[0] - ), - }; + GeneratedTusClientConformanceScenarios.CLIENT_CONFORMANCE_SCENARIOS; private GeneratedTusProtocolContract() { } diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java new file mode 100644 index 00000000..41927ba2 --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -0,0 +1,315 @@ +/* + * 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; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import static org.junit.Assert.assertArrayEquals; + +/** + * Tests generated TUS client runtime event fixtures against the real uploader. + */ +public class TestGeneratedTusRuntimeEvents extends MockServerProvider { + private static final GeneratedTusRuntimeEventCase[] CASES = + new GeneratedTusRuntimeEventCase[] { + new GeneratedTusRuntimeEventCase( + "singleUploadLifecycle", + new GeneratedTusRuntimeEventInput( + "hello world", + "generated-contract", + "absolute", + false, + 11, + new GeneratedTusRuntimeEventMetadata[] { + new GeneratedTusRuntimeEventMetadata( + "filename", + "hello.txt" + ), + } + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "POST", + "endpoint", + 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Location", + "https://tus.io/uploads/generated-contract" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + } + ), + new GeneratedTusRuntimeEventCase( + "relativeLocationResolution", + new GeneratedTusRuntimeEventInput( + "hello world", + "relative-contract", + "relative", + true, + 11, + new GeneratedTusRuntimeEventMetadata[] { + new GeneratedTusRuntimeEventMetadata( + "filename", + "hello.txt" + ), + } + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "POST", + "endpoint", + 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Location", + "relative-contract" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + } + ), + }; + + /** + * Verifies the sync uploader emits generated progress and chunk-complete events. + */ + @Test + public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Exception { + for (GeneratedTusRuntimeEventCase testCase : CASES) { + mockServer.reset(); + + final List events = new ArrayList(); + TusClient client = new TusClient(); + client.setUploadCreationURL(endpointUrlFor(testCase)); + + registerResponses(testCase); + + TusUploader uploader = client.createUpload(uploadFor(testCase)); + uploader.setChunkSize(testCase.input.chunkSize); + uploader.setProgressListener(new TusUploader.ProgressListener() { + @Override + public void onProgress(long bytesSent, long bytesTotal) { + events.add("progress:" + bytesSent + ":" + bytesTotal); + } + }); + uploader.setChunkCompleteListener(new TusUploader.ChunkCompleteListener() { + @Override + public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) { + events.add("chunk-complete:" + chunkSize + ":" + bytesAccepted + ":" + bytesTotal); + } + }); + + while (uploader.uploadChunk() > -1) { + } + uploader.finish(); + + assertArrayEquals( + testCase.scenarioId, + testCase.eventKeys, + events.toArray(new String[events.size()])); + } + } + + private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { + byte[] content = testCase.input.content.getBytes(StandardCharsets.UTF_8); + TusUpload upload = new TusUpload(); + upload.setSize(content.length); + upload.setInputStream(new ByteArrayInputStream(content)); + upload.setMetadata(metadataFor(testCase.input.metadata)); + return upload; + } + + private Map metadataFor(GeneratedTusRuntimeEventMetadata[] metadata) { + Map result = new LinkedHashMap(); + for (GeneratedTusRuntimeEventMetadata entry : metadata) { + result.put(entry.name, entry.value); + } + return result; + } + + private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exception { + for (GeneratedTusRuntimeEventRequest request : testCase.requests) { + HttpRequest httpRequest = new HttpRequest() + .withPath(pathFor(testCase, request)); + if (!"upload".equals(request.url)) { + httpRequest.withMethod(request.method); + } + + mockServer.when(httpRequest).respond(responseFor(testCase, request)); + } + } + + private String pathFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventRequest request) throws Exception { + if ("endpoint".equals(request.url)) { + return endpointUrlFor(testCase).getPath(); + } + + return uploadUrlFor(testCase).getPath(); + } + + private HttpResponse responseFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventRequest request) throws Exception { + HttpResponse response = new HttpResponse().withStatusCode(request.statusCode); + for (GeneratedTusRuntimeEventHeader header : request.headers) { + response.withHeader(header.name, headerValueFor(testCase, header)); + } + return response; + } + + private String headerValueFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventHeader header) throws Exception { + if (!"Location".equals(header.name)) { + return header.value; + } + + if ("relative".equals(testCase.input.locationHeaderKind)) { + return testCase.input.uploadPath; + } + + return uploadUrlFor(testCase).toString(); + } + + private URL uploadUrlFor(GeneratedTusRuntimeEventCase testCase) throws Exception { + return new URL(mockServerURL.toString() + "/" + testCase.input.uploadPath); + } + + private URL endpointUrlFor(GeneratedTusRuntimeEventCase testCase) throws Exception { + if (testCase.input.endpointHasTrailingSlash) { + return new URL(mockServerURL.toString() + "/"); + } + + return mockServerURL; + } + + private static final class GeneratedTusRuntimeEventCase { + final String scenarioId; + final GeneratedTusRuntimeEventInput input; + final GeneratedTusRuntimeEventRequest[] requests; + final String[] eventKeys; + + GeneratedTusRuntimeEventCase( + String scenarioId, + GeneratedTusRuntimeEventInput input, + GeneratedTusRuntimeEventRequest[] requests, + String[] eventKeys) { + this.scenarioId = scenarioId; + this.input = input; + this.requests = requests; + this.eventKeys = eventKeys; + } + } + + private static final class GeneratedTusRuntimeEventInput { + final String content; + final String uploadPath; + final String locationHeaderKind; + final boolean endpointHasTrailingSlash; + final int chunkSize; + final GeneratedTusRuntimeEventMetadata[] metadata; + + GeneratedTusRuntimeEventInput( + String content, + String uploadPath, + String locationHeaderKind, + boolean endpointHasTrailingSlash, + int chunkSize, + GeneratedTusRuntimeEventMetadata[] metadata) { + this.content = content; + this.uploadPath = uploadPath; + this.locationHeaderKind = locationHeaderKind; + this.endpointHasTrailingSlash = endpointHasTrailingSlash; + this.chunkSize = chunkSize; + this.metadata = metadata; + } + } + + private static final class GeneratedTusRuntimeEventRequest { + final String method; + final String url; + final int statusCode; + final GeneratedTusRuntimeEventHeader[] headers; + + GeneratedTusRuntimeEventRequest( + String method, + String url, + int statusCode, + GeneratedTusRuntimeEventHeader[] headers) { + this.method = method; + this.url = url; + this.statusCode = statusCode; + this.headers = headers; + } + } + + private static final class GeneratedTusRuntimeEventHeader { + final String name; + final String value; + + GeneratedTusRuntimeEventHeader(String name, String value) { + this.name = name; + this.value = value; + } + } + + private static final class GeneratedTusRuntimeEventMetadata { + final String name; + final String value; + + GeneratedTusRuntimeEventMetadata(String name, String value) { + this.name = name; + this.value = value; + } + } +} From e892ed197f1deceaa986409fd2bb5157da9adf7a Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 20:27:30 +0200 Subject: [PATCH 22/24] Cover generated resume runtime events --- .../client/TestGeneratedTusRuntimeEvents.java | 136 +++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 41927ba2..8897a811 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -19,6 +19,8 @@ import org.mockserver.model.HttpResponse; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * Tests generated TUS client runtime event fixtures against the real uploader. @@ -34,6 +36,8 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "absolute", false, 11, + null, + false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -71,6 +75,52 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "chunk-complete:11:11:11", } ), + new GeneratedTusRuntimeEventCase( + "resumeFromPreviousUpload", + new GeneratedTusRuntimeEventInput( + "hello world", + "resume-contract", + "stored", + false, + 6, + "contract-resume-fingerprint", + true, + new GeneratedTusRuntimeEventMetadata[0] + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "HEAD", + "upload", + 200, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "5" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), new GeneratedTusRuntimeEventCase( "relativeLocationResolution", new GeneratedTusRuntimeEventInput( @@ -79,6 +129,8 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "relative", true, 11, + null, + false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -129,10 +181,17 @@ public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Except final List events = new ArrayList(); TusClient client = new TusClient(); client.setUploadCreationURL(endpointUrlFor(testCase)); + GeneratedTusRuntimeEventUrlStore urlStore = urlStoreFor(testCase); + if (urlStore != null) { + client.enableResuming(urlStore); + } + if (testCase.input.removeFingerprintOnSuccess) { + client.enableRemoveFingerprintOnSuccess(); + } registerResponses(testCase); - TusUploader uploader = client.createUpload(uploadFor(testCase)); + TusUploader uploader = uploaderFor(client, testCase); uploader.setChunkSize(testCase.input.chunkSize); uploader.setProgressListener(new TusUploader.ProgressListener() { @Override @@ -155,7 +214,17 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) testCase.scenarioId, testCase.eventKeys, events.toArray(new String[events.size()])); + assertStoredUploadState(testCase, urlStore); + } + } + + private TusUploader uploaderFor(TusClient client, GeneratedTusRuntimeEventCase testCase) + throws Exception { + if (testCase.input.fingerprint != null) { + return client.resumeUpload(uploadFor(testCase)); } + + return client.createUpload(uploadFor(testCase)); } private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { @@ -164,6 +233,9 @@ private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { upload.setSize(content.length); upload.setInputStream(new ByteArrayInputStream(content)); upload.setMetadata(metadataFor(testCase.input.metadata)); + if (testCase.input.fingerprint != null) { + upload.setFingerprint(testCase.input.fingerprint); + } return upload; } @@ -179,7 +251,7 @@ private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exc for (GeneratedTusRuntimeEventRequest request : testCase.requests) { HttpRequest httpRequest = new HttpRequest() .withPath(pathFor(testCase, request)); - if (!"upload".equals(request.url)) { + if (!"upload".equals(request.url) || "HEAD".equals(request.method)) { httpRequest.withMethod(request.method); } @@ -233,6 +305,41 @@ private URL endpointUrlFor(GeneratedTusRuntimeEventCase testCase) throws Excepti return mockServerURL; } + private GeneratedTusRuntimeEventUrlStore urlStoreFor( + GeneratedTusRuntimeEventCase testCase) throws Exception { + if (testCase.input.fingerprint == null) { + return null; + } + + GeneratedTusRuntimeEventUrlStore store = new GeneratedTusRuntimeEventUrlStore(); + store.set(testCase.input.fingerprint, uploadUrlFor(testCase)); + return store; + } + + private void assertStoredUploadState( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventUrlStore urlStore) { + if (urlStore == null) { + return; + } + + URL storedUrl = urlStore.get(testCase.input.fingerprint); + if (testCase.input.removeFingerprintOnSuccess) { + assertNull(testCase.scenarioId, storedUrl); + return; + } + + assertEquals(testCase.scenarioId, uploadUrlForUnchecked(testCase), storedUrl); + } + + private URL uploadUrlForUnchecked(GeneratedTusRuntimeEventCase testCase) { + try { + return uploadUrlFor(testCase); + } catch (Exception error) { + throw new AssertionError(error); + } + } + private static final class GeneratedTusRuntimeEventCase { final String scenarioId; final GeneratedTusRuntimeEventInput input; @@ -257,6 +364,8 @@ private static final class GeneratedTusRuntimeEventInput { final String locationHeaderKind; final boolean endpointHasTrailingSlash; final int chunkSize; + final String fingerprint; + final boolean removeFingerprintOnSuccess; final GeneratedTusRuntimeEventMetadata[] metadata; GeneratedTusRuntimeEventInput( @@ -265,12 +374,16 @@ private static final class GeneratedTusRuntimeEventInput { String locationHeaderKind, boolean endpointHasTrailingSlash, int chunkSize, + String fingerprint, + boolean removeFingerprintOnSuccess, GeneratedTusRuntimeEventMetadata[] metadata) { this.content = content; this.uploadPath = uploadPath; this.locationHeaderKind = locationHeaderKind; this.endpointHasTrailingSlash = endpointHasTrailingSlash; this.chunkSize = chunkSize; + this.fingerprint = fingerprint; + this.removeFingerprintOnSuccess = removeFingerprintOnSuccess; this.metadata = metadata; } } @@ -312,4 +425,23 @@ private static final class GeneratedTusRuntimeEventMetadata { this.value = value; } } + + private static final class GeneratedTusRuntimeEventUrlStore implements TusURLStore { + private final Map values = new LinkedHashMap(); + + @Override + public URL get(String fingerprint) { + return values.get(fingerprint); + } + + @Override + public void set(String fingerprint, URL url) { + values.put(fingerprint, url); + } + + @Override + public void remove(String fingerprint) { + values.remove(fingerprint); + } + } } From 420dea9ed968fe2224d5d131a3ca436317fb2785 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 20:44:03 +0200 Subject: [PATCH 23/24] Keep generated runtime canary lint-clean --- .../client/TestGeneratedTusRuntimeEvents.java | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 8897a811..768b6139 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -37,7 +37,6 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { false, 11, null, - false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -83,8 +82,10 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "stored", false, 6, - "contract-resume-fingerprint", - true, + new GeneratedTusRuntimeEventStoredUpload( + "contract-resume-fingerprint", + true + ), new GeneratedTusRuntimeEventMetadata[0] ), new GeneratedTusRuntimeEventRequest[] { @@ -130,7 +131,6 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { true, 11, null, - false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -185,7 +185,9 @@ public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Except if (urlStore != null) { client.enableResuming(urlStore); } - if (testCase.input.removeFingerprintOnSuccess) { + if ( + testCase.input.storedUpload != null + && testCase.input.storedUpload.removeFingerprintOnSuccess) { client.enableRemoveFingerprintOnSuccess(); } @@ -207,6 +209,7 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) }); while (uploader.uploadChunk() > -1) { + continue; } uploader.finish(); @@ -220,7 +223,7 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) private TusUploader uploaderFor(TusClient client, GeneratedTusRuntimeEventCase testCase) throws Exception { - if (testCase.input.fingerprint != null) { + if (testCase.input.storedUpload != null) { return client.resumeUpload(uploadFor(testCase)); } @@ -233,8 +236,8 @@ private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { upload.setSize(content.length); upload.setInputStream(new ByteArrayInputStream(content)); upload.setMetadata(metadataFor(testCase.input.metadata)); - if (testCase.input.fingerprint != null) { - upload.setFingerprint(testCase.input.fingerprint); + if (testCase.input.storedUpload != null) { + upload.setFingerprint(testCase.input.storedUpload.fingerprint); } return upload; } @@ -307,12 +310,12 @@ private URL endpointUrlFor(GeneratedTusRuntimeEventCase testCase) throws Excepti private GeneratedTusRuntimeEventUrlStore urlStoreFor( GeneratedTusRuntimeEventCase testCase) throws Exception { - if (testCase.input.fingerprint == null) { + if (testCase.input.storedUpload == null) { return null; } GeneratedTusRuntimeEventUrlStore store = new GeneratedTusRuntimeEventUrlStore(); - store.set(testCase.input.fingerprint, uploadUrlFor(testCase)); + store.set(testCase.input.storedUpload.fingerprint, uploadUrlFor(testCase)); return store; } @@ -323,8 +326,8 @@ private void assertStoredUploadState( return; } - URL storedUrl = urlStore.get(testCase.input.fingerprint); - if (testCase.input.removeFingerprintOnSuccess) { + URL storedUrl = urlStore.get(testCase.input.storedUpload.fingerprint); + if (testCase.input.storedUpload.removeFingerprintOnSuccess) { assertNull(testCase.scenarioId, storedUrl); return; } @@ -364,8 +367,7 @@ private static final class GeneratedTusRuntimeEventInput { final String locationHeaderKind; final boolean endpointHasTrailingSlash; final int chunkSize; - final String fingerprint; - final boolean removeFingerprintOnSuccess; + final GeneratedTusRuntimeEventStoredUpload storedUpload; final GeneratedTusRuntimeEventMetadata[] metadata; GeneratedTusRuntimeEventInput( @@ -374,17 +376,27 @@ private static final class GeneratedTusRuntimeEventInput { String locationHeaderKind, boolean endpointHasTrailingSlash, int chunkSize, - String fingerprint, - boolean removeFingerprintOnSuccess, + GeneratedTusRuntimeEventStoredUpload storedUpload, GeneratedTusRuntimeEventMetadata[] metadata) { this.content = content; this.uploadPath = uploadPath; this.locationHeaderKind = locationHeaderKind; this.endpointHasTrailingSlash = endpointHasTrailingSlash; this.chunkSize = chunkSize; + this.storedUpload = storedUpload; + this.metadata = metadata; + } + } + + private static final class GeneratedTusRuntimeEventStoredUpload { + final String fingerprint; + final boolean removeFingerprintOnSuccess; + + GeneratedTusRuntimeEventStoredUpload( + String fingerprint, + boolean removeFingerprintOnSuccess) { this.fingerprint = fingerprint; this.removeFingerprintOnSuccess = removeFingerprintOnSuccess; - this.metadata = metadata; } } From 40143841038f51a1dc56aa5db5e5b000e6d85ff3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 22:12:19 +0200 Subject: [PATCH 24/24] Support deferred length uploads --- .../java/io/tus/java/client/TusClient.java | 6 +- .../java/io/tus/java/client/TusUpload.java | 21 ++++ .../java/io/tus/java/client/TusUploader.java | 12 ++ .../client/GeneratedTusProtocolContract.java | 1 + .../client/TestGeneratedTusRuntimeEvents.java | 115 +++++++++++++++++- .../io/tus/java/client/TestTusClient.java | 34 ++++++ .../io/tus/java/client/TestTusUploader.java | 36 ++++++ 7 files changed, 220 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/tus/java/client/TusClient.java b/src/main/java/io/tus/java/client/TusClient.java index 4b56a474..d4df183f 100644 --- a/src/main/java/io/tus/java/client/TusClient.java +++ b/src/main/java/io/tus/java/client/TusClient.java @@ -203,7 +203,11 @@ public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolExcept connection.setRequestProperty("Upload-Metadata", encodedMetadata); } - connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); + if (upload.isUploadLengthDeferred()) { + connection.addRequestProperty("Upload-Defer-Length", "1"); + } else { + connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); + } connection.connect(); int responseCode = connection.getResponseCode(); diff --git a/src/main/java/io/tus/java/client/TusUpload.java b/src/main/java/io/tus/java/client/TusUpload.java index 5559af59..ecd63732 100644 --- a/src/main/java/io/tus/java/client/TusUpload.java +++ b/src/main/java/io/tus/java/client/TusUpload.java @@ -21,6 +21,7 @@ public class TusUpload { private TusInputStream tusInputStream; private String fingerprint; private Map metadata; + private boolean uploadLengthDeferred; /** * Create a new TusUpload object. @@ -62,6 +63,26 @@ public void setSize(long size) { this.size = size; } + /** + * Returns whether upload creation should defer declaring the upload length. + * @return True if the Upload-Defer-Length creation header should be used. + */ + public boolean isUploadLengthDeferred() { + return uploadLengthDeferred; + } + + /** + * Set whether upload creation should defer declaring the upload length. + * + * When enabled, the upload is created with Upload-Defer-Length and the uploader declares + * Upload-Length on the first PATCH request. + * + * @param uploadLengthDeferred True to use deferred upload length creation. + */ + public void setUploadLengthDeferred(boolean uploadLengthDeferred) { + this.uploadLengthDeferred = uploadLengthDeferred; + } + /** * Returns the file specific fingerprint. * @return Fingerprint as String. diff --git a/src/main/java/io/tus/java/client/TusUploader.java b/src/main/java/io/tus/java/client/TusUploader.java index 8af4c462..d3b97d78 100644 --- a/src/main/java/io/tus/java/client/TusUploader.java +++ b/src/main/java/io/tus/java/client/TusUploader.java @@ -56,7 +56,9 @@ public interface ChunkCompleteListener { private int requestPayloadSize = 10 * 1024 * 1024; private int bytesRemainingForRequest; private long requestStartOffset; + private boolean requestDeclaresUploadLength; private boolean requestProgressStarted; + private boolean uploadLengthDeclared; private ProgressListener progressListener; private ChunkCompleteListener chunkCompleteListener; @@ -81,6 +83,7 @@ public TusUploader(TusClient client, TusUpload upload, URL uploadURL, TusInputSt this.offset = offset; this.client = client; this.upload = upload; + uploadLengthDeclared = !upload.isUploadLengthDeferred(); input.seekTo(offset); @@ -94,6 +97,7 @@ private void openConnection() throws IOException, ProtocolException { } bytesRemainingForRequest = requestPayloadSize; + requestDeclaresUploadLength = false; requestStartOffset = offset; requestProgressStarted = false; input.mark(requestPayloadSize); @@ -105,6 +109,10 @@ private void openConnection() throws IOException, ProtocolException { } client.prepareConnection(connection); connection.setRequestProperty("Upload-Offset", Long.toString(offset)); + if (!uploadLengthDeclared) { + connection.setRequestProperty("Upload-Length", Long.toString(upload.getSize())); + requestDeclaresUploadLength = true; + } connection.setRequestProperty("Content-Type", "application/offset+octet-stream"); connection.setRequestProperty("Expect", "100-continue"); @@ -409,8 +417,12 @@ private void finishConnection() throws ProtocolException, IOException { connection); } + if (requestDeclaresUploadLength) { + uploadLengthDeclared = true; + } notifyChunkComplete(serverOffset - requestStartOffset, serverOffset); connection = null; + requestDeclaresUploadLength = false; requestProgressStarted = false; } } diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index e4108fbf..13c1127b 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -670,6 +670,7 @@ final class GeneratedTusProtocolContract { "patchTusUpload", }, new String[] { + "abort-current-request", "concatenate-partial-uploads", "emit-progress", "split-parallel-upload-boundaries", diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 768b6139..284d1f83 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -30,6 +30,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { new GeneratedTusRuntimeEventCase[] { new GeneratedTusRuntimeEventCase( "singleUploadLifecycle", + false, new GeneratedTusRuntimeEventInput( "hello world", "generated-contract", @@ -49,6 +50,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "POST", "endpoint", 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Location", @@ -60,6 +67,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "PATCH", "upload", 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "0" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Offset", @@ -76,6 +89,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "resumeFromPreviousUpload", + false, new GeneratedTusRuntimeEventInput( "hello world", "resume-contract", @@ -93,6 +107,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "HEAD", "upload", 200, + new GeneratedTusRuntimeEventHeader[0], new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Length", @@ -108,6 +123,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "PATCH", "upload", 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "5" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Offset", @@ -124,6 +145,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "relativeLocationResolution", + false, new GeneratedTusRuntimeEventInput( "hello world", "relative-contract", @@ -143,6 +165,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "POST", "endpoint", 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Location", @@ -154,6 +182,75 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "PATCH", "upload", 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "0" + ), + }, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + } + ), + new GeneratedTusRuntimeEventCase( + "deferredLengthUpload", + true, + new GeneratedTusRuntimeEventInput( + "hello world", + "deferred-contract", + "absolute", + false, + 100, + null, + new GeneratedTusRuntimeEventMetadata[] { + new GeneratedTusRuntimeEventMetadata( + "filename", + "hello.txt" + ), + } + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "POST", + "endpoint", + 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Defer-Length", + "1" + ), + }, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Location", + "https://tus.io/uploads/deferred-contract" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "0" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Offset", @@ -239,6 +336,7 @@ private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { if (testCase.input.storedUpload != null) { upload.setFingerprint(testCase.input.storedUpload.fingerprint); } + upload.setUploadLengthDeferred(testCase.uploadLengthDeferred); return upload; } @@ -257,6 +355,9 @@ private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exc if (!"upload".equals(request.url) || "HEAD".equals(request.method)) { httpRequest.withMethod(request.method); } + for (GeneratedTusRuntimeEventHeader header : request.requestHeaders) { + httpRequest.withHeader(header.name, header.value); + } mockServer.when(httpRequest).respond(responseFor(testCase, request)); } @@ -276,7 +377,7 @@ private HttpResponse responseFor( GeneratedTusRuntimeEventCase testCase, GeneratedTusRuntimeEventRequest request) throws Exception { HttpResponse response = new HttpResponse().withStatusCode(request.statusCode); - for (GeneratedTusRuntimeEventHeader header : request.headers) { + for (GeneratedTusRuntimeEventHeader header : request.responseHeaders) { response.withHeader(header.name, headerValueFor(testCase, header)); } return response; @@ -345,16 +446,19 @@ private URL uploadUrlForUnchecked(GeneratedTusRuntimeEventCase testCase) { private static final class GeneratedTusRuntimeEventCase { final String scenarioId; + final boolean uploadLengthDeferred; final GeneratedTusRuntimeEventInput input; final GeneratedTusRuntimeEventRequest[] requests; final String[] eventKeys; GeneratedTusRuntimeEventCase( String scenarioId, + boolean uploadLengthDeferred, GeneratedTusRuntimeEventInput input, GeneratedTusRuntimeEventRequest[] requests, String[] eventKeys) { this.scenarioId = scenarioId; + this.uploadLengthDeferred = uploadLengthDeferred; this.input = input; this.requests = requests; this.eventKeys = eventKeys; @@ -404,17 +508,20 @@ private static final class GeneratedTusRuntimeEventRequest { final String method; final String url; final int statusCode; - final GeneratedTusRuntimeEventHeader[] headers; + final GeneratedTusRuntimeEventHeader[] requestHeaders; + final GeneratedTusRuntimeEventHeader[] responseHeaders; GeneratedTusRuntimeEventRequest( String method, String url, int statusCode, - GeneratedTusRuntimeEventHeader[] headers) { + GeneratedTusRuntimeEventHeader[] requestHeaders, + GeneratedTusRuntimeEventHeader[] responseHeaders) { this.method = method; this.url = url; this.statusCode = statusCode; - this.headers = headers; + this.requestHeaders = requestHeaders; + this.responseHeaders = responseHeaders; } } diff --git a/src/test/java/io/tus/java/client/TestTusClient.java b/src/test/java/io/tus/java/client/TestTusClient.java index 9bd1ec5a..33211f2b 100644 --- a/src/test/java/io/tus/java/client/TestTusClient.java +++ b/src/test/java/io/tus/java/client/TestTusClient.java @@ -107,6 +107,40 @@ public void testCreateUpload() throws IOException, ProtocolException { assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); } + + /** + * Verifies if uploads can be created with deferred upload length. + * @throws IOException if upload data cannot be read. + * @throws ProtocolException if the upload cannot be constructed. + */ + @Test + public void testCreateUploadWithDeferredLength() throws IOException, ProtocolException { + mockServer.when(new HttpRequest() + .withMethod("POST") + .withPath("/files") + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Upload-Defer-Length", "1")) + .respond(new HttpResponse() + .withStatusCode(201) + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Location", mockServerURL + "/foo")); + + TusClient client = new TusClient(); + client.setUploadCreationURL(mockServerURL); + TusUpload upload = new TusUpload(); + upload.setSize(10); + upload.setUploadLengthDeferred(true); + upload.setInputStream(new ByteArrayInputStream(new byte[10])); + TusUploader uploader = client.createUpload(upload); + HttpRequest[] requests = mockServer.retrieveRecordedRequests(new HttpRequest() + .withMethod("POST") + .withPath("/files")); + + assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); + assertEquals(1, requests.length); + assertFalse(requests[0].containsHeader("Upload-Length")); + } + /** * Verifies if uploads can be created with the tus client through a proxy. * @throws IOException if upload data cannot be read. diff --git a/src/test/java/io/tus/java/client/TestTusUploader.java b/src/test/java/io/tus/java/client/TestTusUploader.java index b81e2b75..b1a7b70c 100644 --- a/src/test/java/io/tus/java/client/TestTusUploader.java +++ b/src/test/java/io/tus/java/client/TestTusUploader.java @@ -74,6 +74,42 @@ public void testTusUploader() throws IOException, ProtocolException { uploader.finish(); } + /** + * Tests if deferred-length uploads declare the upload length on the first PATCH request. + * @throws IOException + * @throws ProtocolException + */ + @Test + public void testTusUploaderDeclaresDeferredLength() throws IOException, ProtocolException { + byte[] content = "hello world".getBytes(); + + mockServer.when(new HttpRequest() + .withPath("/files/deferred") + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Upload-Length", "11") + .withHeader("Upload-Offset", "0") + .withHeader("Content-Type", "application/offset+octet-stream") + .withBody(content)) + .respond(new HttpResponse() + .withStatusCode(204) + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Upload-Offset", "11")); + + TusClient client = new TusClient(); + URL uploadUrl = new URL(mockServerURL + "/deferred"); + TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); + TusUpload upload = new TusUpload(); + upload.setSize(11); + upload.setUploadLengthDeferred(true); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); + + uploader.setChunkSize(100); + assertEquals(11, uploader.uploadChunk()); + assertEquals(-1, uploader.uploadChunk()); + uploader.finish(); + } + /** * Tests if the {@link TusUploader} actually uploads files through a proxy. * @throws IOException