diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 866e680..332d58b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,16 +1,17 @@ name: CI on: + workflow_dispatch: push: branches: - main pull_request: types: - opened + - ready_for_review - synchronize - unlabeled - runs-on: ubuntu-latest jobs: android: strategy: @@ -21,6 +22,12 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Checkout tus-java-client companion branch + uses: actions/checkout@v6 + with: + repository: tus/tus-java-client + ref: tus-gen + path: tus-java-client - name: set up JDK ${{ matrix.java }} uses: actions/setup-java@v5 with: diff --git a/.gitignore b/.gitignore index 9d28fe9..4854a52 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ /.idea/libraries .DS_Store /build +/tus-java-client *.iml .idea/ diff --git a/settings.gradle b/settings.gradle index 9aae33e..5ab73d3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,9 @@ include ':example', ':tus-android-client' + +def companionTusJavaClient = [ + file('../tus-java-client'), + file('tus-java-client') +].find { it.exists() } +if (companionTusJavaClient != null) { + includeBuild(companionTusJavaClient) +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockScenario.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockScenario.java new file mode 100644 index 0000000..11c3fc8 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockScenario.java @@ -0,0 +1,599 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.robolectric.shadows.ShadowContentResolver; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class Api2DevdockScenario { + private static final String PROVIDER_AUTHORITY = "io.tus.android.client.api2devdock"; + + private Api2DevdockScenario() { + } + + static final class UploadCallbackEventKinds { + final String chunkComplete; + final String progress; + final String sourceClose; + final String success; + final String uploadUrlAvailable; + + UploadCallbackEventKinds(JSONObject eventKinds) throws JSONException { + chunkComplete = eventKinds.getString("chunkComplete"); + progress = eventKinds.getString("progress"); + sourceClose = eventKinds.getString("sourceClose"); + success = eventKinds.getString("success"); + uploadUrlAvailable = eventKinds.getString("uploadUrlAvailable"); + } + } + + static final class UploadCallbacksPlan { + final List allowedExtraEventKeyPrefixes; + final List> eventKeyAlternativeGroups; + final UploadCallbackEventKinds eventKinds; + final String eventKeyPartSeparator; + final List eventKeys; + final String eventPolicyMatching; + + UploadCallbacksPlan(JSONObject uploadCallbacks) throws JSONException { + allowedExtraEventKeyPrefixes = stringList( + uploadCallbacks.getJSONArray("allowedExtraEventKeyPrefixes") + ); + eventKeyAlternativeGroups = stringListList( + uploadCallbacks.getJSONArray("eventKeyAlternativeGroups") + ); + eventKinds = new UploadCallbackEventKinds(uploadCallbacks.getJSONObject("eventKinds")); + eventKeyPartSeparator = uploadCallbacks.getString("eventKeyPartSeparator"); + eventKeys = stringList(uploadCallbacks.getJSONArray("eventKeys")); + eventPolicyMatching = uploadCallbacks.getString("eventPolicyMatching"); + } + } + + static final class TerminationPlan { + final int expectedVerificationStatus; + final String method; + final int minimumDeleteRequestCount; + final int stopAfterAcceptedBytes; + final String verificationMethod; + + TerminationPlan(JSONObject termination) throws JSONException { + expectedVerificationStatus = termination.getInt("expectedVerificationStatus"); + method = termination.getString("method"); + minimumDeleteRequestCount = termination.getInt("minimumDeleteRequestCount"); + stopAfterAcceptedBytes = termination.getInt("stopAfterAcceptedBytes"); + verificationMethod = termination.getString("verificationMethod"); + } + } + + static final class RequestLifecycleHooksPlan { + final List expectedAfterResponseMethods; + final List expectedAfterResponseStatusCodes; + final List expectedBeforeRequestMethods; + final List ignoredRequestMethods; + + RequestLifecycleHooksPlan(JSONObject requestLifecycleHooks) throws JSONException { + expectedAfterResponseMethods = stringList( + requestLifecycleHooks.getJSONArray("expectedAfterResponseMethods") + ); + expectedAfterResponseStatusCodes = integerList( + requestLifecycleHooks.getJSONArray("expectedAfterResponseStatusCodes") + ); + expectedBeforeRequestMethods = stringList( + requestLifecycleHooks.getJSONArray("expectedBeforeRequestMethods") + ); + ignoredRequestMethods = stringList( + requestLifecycleHooks.getJSONArray("ignoredRequestMethods") + ); + } + } + + static final class RetryOffsetRecoveryFailAfterResponsePlan { + final String message; + final String method; + final int occurrence; + + RetryOffsetRecoveryFailAfterResponsePlan(JSONObject failAfterResponse) + throws JSONException { + message = failAfterResponse.getString("message"); + method = failAfterResponse.getString("method"); + occurrence = failAfterResponse.getInt("occurrence"); + } + } + + static final class RetryOffsetRecoveryRecoveryResponsePlan { + final String method; + final String offsetHeader; + + RetryOffsetRecoveryRecoveryResponsePlan(JSONObject recoveryResponse) + throws JSONException { + method = recoveryResponse.getString("method"); + offsetHeader = recoveryResponse.getString("offsetHeader"); + } + } + + static final class RetryOffsetRecoveryPlan { + final int expectedFailureCount; + final int expectedRecoveredOffset; + final int expectedRecoveryRequestCount; + final List expectedRequestMethods; + final RetryOffsetRecoveryFailAfterResponsePlan failAfterResponse; + final RetryOffsetRecoveryRecoveryResponsePlan recoveryResponse; + + RetryOffsetRecoveryPlan(JSONObject retryOffsetRecovery) throws JSONException { + expectedFailureCount = retryOffsetRecovery.getInt("expectedFailureCount"); + expectedRecoveredOffset = retryOffsetRecovery.getInt("expectedRecoveredOffset"); + expectedRecoveryRequestCount = + retryOffsetRecovery.getInt("expectedRecoveryRequestCount"); + expectedRequestMethods = + stringList(retryOffsetRecovery.getJSONArray("expectedRequestMethods")); + failAfterResponse = new RetryOffsetRecoveryFailAfterResponsePlan( + retryOffsetRecovery.getJSONObject("failAfterResponse") + ); + recoveryResponse = new RetryOffsetRecoveryRecoveryResponsePlan( + retryOffsetRecovery.getJSONObject("recoveryResponse") + ); + } + } + + static TusAndroidUpload androidUpload( + Activity activity, + Uri uri, + JSONObject scenario, + JSONObject createResponse, + String fingerprint + ) throws IOException, JSONException { + final TusAndroidUpload upload = new TusAndroidUpload(uri, activity); + upload.setFingerprint(fingerprint); + upload.setMetadata(uploadMetadata( + scenario.getJSONObject("upload"), + scenario, + createResponse + )); + + return upload; + } + + static int fixedChunkSizeBytes(JSONObject uploadConfig) throws JSONException { + final JSONObject chunkSize = uploadConfig.getJSONObject("chunkSize"); + final String kind = chunkSize.getString("kind"); + if (!"fixed-bytes".equals(kind)) { + throw new IllegalArgumentException("unsupported chunk size kind " + kind); + } + + return chunkSize.getInt("bytes"); + } + + static List matchUploadCallbackEventKeys( + UploadCallbacksPlan plan, + List actual + ) { + if (!"exact".equals(plan.eventPolicyMatching) + && !"exact-except-allowed-extra-events".equals(plan.eventPolicyMatching)) { + throw new IllegalArgumentException( + "unsupported upload callback event policy " + plan.eventPolicyMatching + ); + } + + final List matched = new ArrayList(); + int expectedIndex = 0; + for (String event : actual) { + if (expectedIndex < plan.eventKeys.size() + && uploadCallbackEventMatchesExpected(plan, expectedIndex, event)) { + matched.add(plan.eventKeys.get(expectedIndex)); + expectedIndex += 1; + continue; + } + + if ("exact-except-allowed-extra-events".equals(plan.eventPolicyMatching) + && hasAllowedUploadCallbackExtraEventPrefix(plan, event)) { + continue; + } + + throw new IllegalStateException( + "unexpected upload callback event " + + event + + " at expected index " + + expectedIndex + + "; expected " + + plan.eventKeys + + ", actual " + + actual + ); + } + + if (expectedIndex != plan.eventKeys.size()) { + throw new IllegalStateException( + "missing upload callback events after index " + + expectedIndex + + "; expected " + + plan.eventKeys + + ", actual " + + actual + ); + } + + return matched; + } + + static void requireFullFileChunkSize(JSONObject uploadConfig) throws JSONException { + final Object chunkSize = uploadConfig.get("chunkSize"); + if (!"full-file".equals(chunkSize)) { + throw new IllegalArgumentException("unsupported chunk size policy " + chunkSize); + } + } + + static JSONObject loadScenario(String scenarioPath) throws IOException, JSONException { + final byte[] contents = Files.readAllBytes(Paths.get(scenarioPath)); + return new JSONObject(new String(contents, StandardCharsets.UTF_8)); + } + + static Uri registerContentUri( + Activity activity, + byte[] content, + String sourceName + ) throws IOException { + final File source = new File(activity.getCacheDir(), sourceName); + Files.write(source.toPath(), content); + + final Uri uri = Uri.parse("content://" + PROVIDER_AUTHORITY + "/" + sourceName); + final Api2DevdockContentProvider provider = new Api2DevdockContentProvider(source); + final ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = PROVIDER_AUTHORITY; + provider.attachInfo(activity, providerInfo); + ShadowContentResolver.registerProviderInternal( + PROVIDER_AUTHORITY, + provider + ); + + return uri; + } + + static byte[] scenarioBytes(JSONObject uploadConfig) throws JSONException { + final JSONObject source = uploadConfig.getJSONObject("source"); + final String kind = source.getString("kind"); + if (!"bytes".equals(kind)) { + throw new IllegalArgumentException("unsupported source kind " + kind); + } + + final String encoding = source.getString("encoding"); + if (!"utf8".equals(encoding)) { + throw new IllegalArgumentException("unsupported source encoding " + encoding); + } + + return source.getString("value").getBytes(StandardCharsets.UTF_8); + } + + static String scenarioPath() { + final String scenarioPath = System.getenv("API2_SDK_EXAMPLE_SCENARIO"); + if (scenarioPath != null && !scenarioPath.isEmpty()) { + return scenarioPath; + } + + final String defaultPath = "tus-android-client/api2-scenario.json"; + if (Files.exists(Paths.get(defaultPath))) { + return defaultPath; + } + + return null; + } + + static URL tusUrl( + JSONObject uploadConfig, + JSONObject scenario, + JSONObject createResponse + ) throws JSONException, java.net.MalformedURLException { + return new URL(scalarString( + resolveValue(uploadConfig.getJSONObject("tusUrl"), scenario, createResponse) + )); + } + + static UploadCallbacksPlan uploadCallbacks(JSONObject scenario) throws JSONException { + return new UploadCallbacksPlan( + scenario.getJSONObject("upload").getJSONObject("uploadCallbacks") + ); + } + + static TerminationPlan termination(JSONObject uploadConfig) throws JSONException { + return new TerminationPlan(uploadConfig.getJSONObject("termination")); + } + + static boolean uploadAddRequestId(JSONObject uploadConfig) throws JSONException { + return uploadConfig.getBoolean("addRequestId"); + } + + static Map> uploadBodyHeadersByMethod( + JSONObject uploadConfig + ) throws JSONException { + final JSONObject bodyHeadersByMethod = uploadConfig.getJSONObject("bodyHeadersByMethod"); + final Map> result = + new LinkedHashMap>(); + final JSONArray methods = bodyHeadersByMethod.names(); + if (methods == null) { + return result; + } + + for (int index = 0; index < methods.length(); index++) { + final String method = methods.getString(index); + result.put(method, stringMap(bodyHeadersByMethod.getJSONObject(method))); + } + + return result; + } + + static Map uploadHeaders(JSONObject uploadConfig) throws JSONException { + return stringMap(uploadConfig.getJSONObject("headers")); + } + + static String uploadRequestIdHeaderName(JSONObject uploadConfig) throws JSONException { + return uploadConfig.getString("requestIdHeaderName"); + } + + static RequestLifecycleHooksPlan requestLifecycleHooks(JSONObject uploadConfig) + throws JSONException { + return new RequestLifecycleHooksPlan(uploadConfig.getJSONObject("requestLifecycleHooks")); + } + + static RetryOffsetRecoveryPlan retryOffsetRecovery(JSONObject uploadConfig) + throws JSONException { + return new RetryOffsetRecoveryPlan(uploadConfig.getJSONObject("retryOffsetRecovery")); + } + + static String uploadCallbackEventKey(UploadCallbacksPlan plan, String... parts) { + final StringBuilder key = new StringBuilder(); + for (int index = 0; index < parts.length; index++) { + if (index > 0) { + key.append(plan.eventKeyPartSeparator); + } + key.append(parts[index]); + } + + return key.toString(); + } + + static String uploadCallbackEventKeyNumber(long value) { + return Long.toString(value); + } + + static String uploadCallbackEventKeyTotal(long value) { + return scalarString(value); + } + + static void writeResult(JSONObject result) throws IOException, JSONException { + final String resultPath = System.getenv("API2_SDK_EXAMPLE_RESULT"); + if (resultPath == null || resultPath.isEmpty()) { + return; + } + + Files.write( + Paths.get(resultPath), + (result.toString(2) + "\n").getBytes(StandardCharsets.UTF_8) + ); + } + + private static Object readPath(Object value, JSONArray pathParts) throws JSONException { + Object current = value; + for (int index = 0; index < pathParts.length(); index++) { + final Object part = pathParts.get(index); + if (current instanceof JSONObject && part instanceof String) { + current = ((JSONObject) current).get((String) part); + continue; + } + + if (current instanceof JSONArray && part instanceof Number) { + current = ((JSONArray) current).get(((Number) part).intValue()); + continue; + } + + throw new IllegalArgumentException("cannot read scenario path part " + part); + } + + return current; + } + + private static Object resolveValue( + JSONObject valueSpec, + JSONObject scenario, + JSONObject createResponse + ) throws JSONException { + if (valueSpec.has("value")) { + return valueSpec.get("value"); + } + + final JSONObject source = valueSpec.getJSONObject("source"); + final String root = source.getString("root"); + final Object rootValue; + if ("scenario".equals(root)) { + rootValue = scenario; + } else if ("createResponse".equals(root)) { + rootValue = createResponse; + } else { + throw new IllegalArgumentException("unsupported scenario value root " + root); + } + + return readPath(rootValue, source.getJSONArray("path")); + } + + private static String scalarString(Object value) { + if (JSONObject.NULL.equals(value)) { + return "null"; + } + + return String.valueOf(value); + } + + private static Map uploadMetadata( + JSONObject uploadConfig, + JSONObject scenario, + JSONObject createResponse + ) throws JSONException { + final JSONArray fields = uploadConfig.getJSONArray("metadata"); + final Map metadata = new LinkedHashMap(); + for (int index = 0; index < fields.length(); index++) { + final JSONObject field = fields.getJSONObject(index); + metadata.put( + field.getString("name"), + scalarString(resolveValue( + field.getJSONObject("value"), + scenario, + createResponse + )) + ); + } + + return metadata; + } + + private static boolean hasAllowedUploadCallbackExtraEventPrefix( + UploadCallbacksPlan plan, + String event + ) { + for (String prefix : plan.allowedExtraEventKeyPrefixes) { + if (event.startsWith(prefix)) { + return true; + } + } + + return false; + } + + private static List stringList(JSONArray values) throws JSONException { + final List result = new ArrayList(); + for (int index = 0; index < values.length(); index++) { + result.add(values.getString(index)); + } + + return result; + } + + private static List integerList(JSONArray values) throws JSONException { + final List result = new ArrayList(); + for (int index = 0; index < values.length(); index++) { + result.add(values.getInt(index)); + } + + return result; + } + + private static List> stringListList(JSONArray values) throws JSONException { + final List> result = new ArrayList>(); + for (int index = 0; index < values.length(); index++) { + result.add(stringList(values.getJSONArray(index))); + } + + return result; + } + + private static Map stringMap(JSONObject values) throws JSONException { + final Map result = new LinkedHashMap(); + final JSONArray names = values.names(); + if (names == null) { + return result; + } + + for (int index = 0; index < names.length(); index++) { + final String name = names.getString(index); + result.put(name, values.getString(name)); + } + + return result; + } + + private static boolean uploadCallbackEventMatchesExpected( + UploadCallbacksPlan plan, + int expectedIndex, + String event + ) { + if (plan.eventKeys.get(expectedIndex).equals(event)) { + return true; + } + + if (expectedIndex >= plan.eventKeyAlternativeGroups.size()) { + return false; + } + + final List alternatives = plan.eventKeyAlternativeGroups.get(expectedIndex); + for (String alternative : alternatives) { + if (alternative.equals(event)) { + return true; + } + } + + return false; + } + + private static final class Api2DevdockContentProvider extends ContentProvider { + private final File source; + + Api2DevdockContentProvider(File source) { + this.source = source; + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder + ) { + final MatrixCursor cursor = new MatrixCursor( + new String[]{OpenableColumns.SIZE, OpenableColumns.DISPLAY_NAME} + ); + cursor.addRow(new Object[]{source.length(), source.getName()}); + + return cursor; + } + + @Override + public String getType(Uri uri) { + return "text/plain"; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return ParcelFileDescriptor.open(source, ParcelFileDescriptor.MODE_READ_ONLY); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusCreationWithUploadExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusCreationWithUploadExampleTest.java new file mode 100644 index 0000000..6400ccf --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusCreationWithUploadExampleTest.java @@ -0,0 +1,105 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusCreationWithUploadExampleTest { + @Test + public void createsAndroidContentUriUploadWithCreationData() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithCreationData(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertEquals( + Api2DevdockScenario.scenarioBytes(scenario.getJSONObject("upload")).length, + result.getInt("acceptedBytes") + ); + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithCreationData( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + Api2DevdockScenario.requireFullFileChunkSize(uploadConfig); + if (!uploadConfig.getBoolean("uploadDataDuringCreation")) { + throw new IllegalStateException( + "creation-with-upload scenario must set uploadDataDuringCreation" + ); + } + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-creation-with-upload.txt" + ); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-creation-with-upload" + ); + + final TusUploader uploader = client.createUploadWithData(upload, content.length); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + return new JSONObject() + .put("acceptedBytes", (int) uploader.getOffset()) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusCreationWithUpload.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusCustomRequestHeadersExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusCustomRequestHeadersExampleTest.java new file mode 100644 index 0000000..051593c --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusCustomRequestHeadersExampleTest.java @@ -0,0 +1,156 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusRequestLifecycleHooks; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusCustomRequestHeadersExampleTest { + @Test + public void observesAndroidTusCustomRequestHeaders() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithCustomHeaders(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithCustomHeaders( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final Map expectedHeaders = + Api2DevdockScenario.uploadHeaders(uploadConfig); + final Map> headersByMethod = + new LinkedHashMap>(); + Api2DevdockScenario.requireFullFileChunkSize(uploadConfig); + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-custom-request-headers.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-custom-request-headers", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.setHeaders(expectedHeaders); + client.setRequestLifecycleHooks(new TusRequestLifecycleHooks( + new TusRequestLifecycleHooks.BeforeRequest() { + @Override + public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) { + if ("POST".equals(context.getMethod()) + || "PATCH".equals(context.getMethod())) { + headersByMethod.put( + context.getMethod(), + observedCustomHeaders( + context.getConnection(), + expectedHeaders + ) + ); + } + } + }, + null + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-custom-request-headers" + ); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(content.length); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + return new JSONObject() + .put("headersByMethod", new JSONObject(headersByMethod)) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static Map observedCustomHeaders( + HttpURLConnection connection, + Map expectedHeaders + ) { + final Map headers = new LinkedHashMap(); + for (Map.Entry entry : expectedHeaders.entrySet()) { + final String value = connection.getRequestProperty(entry.getKey()); + if (value == null) { + throw new IllegalStateException( + "custom request headers did not observe " + entry.getKey() + ); + } + + headers.put(entry.getKey(), value); + } + + return headers; + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusCustomRequestHeaders.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusDeferredLengthUploadExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusDeferredLengthUploadExampleTest.java new file mode 100644 index 0000000..137194b --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusDeferredLengthUploadExampleTest.java @@ -0,0 +1,111 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusDeferredLengthUploadExampleTest { + @Test + public void uploadsAndroidContentUriWithDeferredLength() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithDeferredLength(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertEquals( + Api2DevdockScenario.scenarioBytes(scenario.getJSONObject("upload")).length, + result.getInt("acceptedBytes") + ); + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithDeferredLength( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final int chunkSize = Api2DevdockScenario.fixedChunkSizeBytes(uploadConfig); + if (!uploadConfig.getBoolean("uploadLengthDeferred")) { + throw new IllegalStateException( + "deferred-length scenario must set uploadLengthDeferred" + ); + } + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-deferred-length-upload.txt" + ); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-deferred-length" + ); + upload.setUploadLengthDeferred(true); + + final TusUploader uploader = client.createUpload(upload); + uploader.setChunkSize(chunkSize); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + return new JSONObject() + .put("acceptedBytes", (int) uploader.getOffset()) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusDeferredLengthUpload.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRequestIdHeadersExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRequestIdHeadersExampleTest.java new file mode 100644 index 0000000..038ea49 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRequestIdHeadersExampleTest.java @@ -0,0 +1,162 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusRequestLifecycleHooks; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusRequestIdHeadersExampleTest { + @Test + public void observesAndroidTusRequestIdHeaders() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithRequestIdHeaders(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithRequestIdHeaders( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final String requestIdHeaderName = + Api2DevdockScenario.uploadRequestIdHeaderName(uploadConfig); + final Map> headersByMethod = + new LinkedHashMap>(); + Api2DevdockScenario.requireFullFileChunkSize(uploadConfig); + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-request-id-headers.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-request-id-headers", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.setHeaders(Api2DevdockScenario.uploadHeaders(uploadConfig)); + if (Api2DevdockScenario.uploadAddRequestId(uploadConfig)) { + client.enableRequestIdHeader(); + } + client.setRequestLifecycleHooks(new TusRequestLifecycleHooks( + new TusRequestLifecycleHooks.BeforeRequest() { + @Override + public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) { + if ("POST".equals(context.getMethod()) + || "PATCH".equals(context.getMethod())) { + final Map headers = + new LinkedHashMap(); + headers.put( + requestIdHeaderName, + observedRequestIdHeader( + context.getConnection(), + context.getMethod(), + requestIdHeaderName + ) + ); + headersByMethod.put(context.getMethod(), headers); + } + } + }, + null + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-request-id-headers" + ); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(content.length); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + return new JSONObject() + .put("headersByMethod", new JSONObject(headersByMethod)) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static String observedRequestIdHeader( + HttpURLConnection connection, + String method, + String requestIdHeaderName + ) { + final String value = connection.getRequestProperty(requestIdHeaderName); + if (value == null || value.isEmpty()) { + throw new IllegalStateException( + "request ID headers did not observe " + + requestIdHeaderName + + " on " + + method + ); + } + + return value; + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusRequestIdHeaders.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRequestLifecycleHooksExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRequestLifecycleHooksExampleTest.java new file mode 100644 index 0000000..42db544 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRequestLifecycleHooksExampleTest.java @@ -0,0 +1,227 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusRequestLifecycleHooks; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusRequestLifecycleHooksExampleTest { + @Test + public void observesAndroidTusRequestLifecycleHooks() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithRequestLifecycleHooks( + activity, + scenario, + createResponse + ); + Api2DevdockScenario.writeResult(result); + + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithRequestLifecycleHooks( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final Api2DevdockScenario.RequestLifecycleHooksPlan plan = + Api2DevdockScenario.requestLifecycleHooks(uploadConfig); + final List beforeRequestMethods = new ArrayList(); + final List afterResponseMethods = new ArrayList(); + final List afterResponseStatusCodes = new ArrayList(); + Api2DevdockScenario.requireFullFileChunkSize(uploadConfig); + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-request-lifecycle-hooks.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-request-lifecycle-hooks", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.setRequestLifecycleHooks(new TusRequestLifecycleHooks( + new TusRequestLifecycleHooks.BeforeRequest() { + @Override + public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) { + if (!plan.ignoredRequestMethods.contains(context.getMethod())) { + beforeRequestMethods.add(context.getMethod()); + } + } + }, + new TusRequestLifecycleHooks.AfterResponse() { + @Override + public void afterResponse( + TusRequestLifecycleHooks.RequestContext context + ) throws IOException { + if (!plan.ignoredRequestMethods.contains(context.getMethod())) { + afterResponseMethods.add(context.getMethod()); + afterResponseStatusCodes.add( + context.getConnection().getResponseCode() + ); + } + } + } + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-request-lifecycle-hooks" + ); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(content.length); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + assertStringList( + beforeRequestMethods, + plan.expectedBeforeRequestMethods, + "before request methods" + ); + assertStringList( + afterResponseMethods, + plan.expectedAfterResponseMethods, + "after response methods" + ); + assertIntegerList( + afterResponseStatusCodes, + plan.expectedAfterResponseStatusCodes, + "after response status codes" + ); + + return new JSONObject() + .put("afterResponseMethods", new JSONArray(afterResponseMethods)) + .put("afterResponseStatusCodes", new JSONArray(afterResponseStatusCodes)) + .put("beforeRequestMethods", new JSONArray(beforeRequestMethods)) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static void assertStringList( + List actual, + List expected, + String label + ) { + if (actual.size() != expected.size()) { + throw new IllegalStateException( + "request lifecycle hooks expected " + + label + + " " + + expected + + ", got " + + actual + ); + } + + for (int index = 0; index < expected.size(); index++) { + if (!actual.get(index).equals(expected.get(index))) { + throw new IllegalStateException( + "request lifecycle hooks expected " + + label + + " " + + expected.get(index) + + " at index " + + index + + ", got " + + actual.get(index) + ); + } + } + } + + private static void assertIntegerList( + List actual, + List expected, + String label + ) { + if (actual.size() != expected.size()) { + throw new IllegalStateException( + "request lifecycle hooks expected " + + label + + " " + + expected + + ", got " + + actual + ); + } + + for (int index = 0; index < expected.size(); index++) { + if (actual.get(index).intValue() != expected.get(index).intValue()) { + throw new IllegalStateException( + "request lifecycle hooks expected " + + label + + " " + + expected.get(index) + + " at index " + + index + + ", got " + + actual.get(index) + ); + } + } + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusRequestLifecycleHooks.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusResumeUploadExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusResumeUploadExampleTest.java new file mode 100644 index 0000000..f0e2d8c --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusResumeUploadExampleTest.java @@ -0,0 +1,147 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.net.URL; + +import io.tus.java.client.FingerprintNotFoundException; +import io.tus.java.client.ProtocolException; +import io.tus.java.client.ResumingNotEnabledException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusResumeUploadExampleTest { + @Test + public void resumesAndroidContentUriUploadFromStoredUrl() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithStoredResume(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertEquals(result.getString("firstUploadUrl"), result.getString("uploadUrl")); + } + + private static JSONObject uploadWithStoredResume( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws FingerprintNotFoundException, IOException, JSONException, ProtocolException, + ResumingNotEnabledException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final JSONObject resumeConfig = uploadConfig.getJSONObject("resume"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-resume-upload.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-resume-upload", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusPreferencesURLStore store = new TusPreferencesURLStore(preferences); + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(store); + if (resumeConfig.getBoolean("removeFingerprintOnSuccess")) { + client.enableRemoveFingerprintOnSuccess(); + } + + final String fingerprint = resumeConfig.getString("fingerprint"); + final TusUploader firstUploader = client.createUpload(Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + fingerprint + )); + firstUploader.setChunkSize(Api2DevdockScenario.fixedChunkSizeBytes(uploadConfig)); + final int firstAcceptedBytes = firstUploader.uploadChunk(); + assertEquals(resumeConfig.getInt("stopAfterAcceptedBytes"), firstAcceptedBytes); + assertEquals(resumeConfig.getInt("stopAfterAcceptedBytes"), firstUploader.getOffset()); + assertNotNull(firstUploader.getUploadURL()); + final String firstUploadUrl = firstUploader.getUploadURL().toString(); + firstUploader.finish(false); + + final URL storedUploadUrl = store.get(fingerprint); + assertNotNull(storedUploadUrl); + assertEquals(firstUploadUrl, storedUploadUrl.toString()); + final int previousUploadCount = preferences.getAll().size(); + assertEquals(resumeConfig.getInt("expectedPreviousUploadCount"), previousUploadCount); + + final TusUploader resumedUploader = client.resumeUpload(Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + fingerprint + )); + resumedUploader.setChunkSize(content.length); + int uploadedChunkSize; + do { + uploadedChunkSize = resumedUploader.uploadChunk(); + } while (uploadedChunkSize > -1); + resumedUploader.finish(); + + assertEquals(content.length, resumedUploader.getOffset()); + assertNotNull(resumedUploader.getUploadURL()); + final String uploadUrl = resumedUploader.getUploadURL().toString(); + final int remainingPreviousUploadCount = preferences.getAll().size(); + assertEquals( + resumeConfig.getInt("expectedRemainingPreviousUploadCount"), + remainingPreviousUploadCount + ); + + final JSONObject result = new JSONObject(); + result.put("firstUploadUrl", firstUploadUrl); + result.put("previousUploadCount", previousUploadCount); + result.put("remainingPreviousUploadCount", remainingPreviousUploadCount); + result.put("uploadUrl", uploadUrl); + + return result; + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusResumeUpload.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRetryOffsetRecoveryExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRetryOffsetRecoveryExampleTest.java new file mode 100644 index 0000000..3989eac --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusRetryOffsetRecoveryExampleTest.java @@ -0,0 +1,235 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusExecutor; +import io.tus.java.client.TusRequestLifecycleHooks; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusRetryOffsetRecoveryExampleTest { + @Test + public void recoversAndroidTusOffsetAfterRetry() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithRetryOffsetRecovery( + activity, + scenario, + createResponse + ); + Api2DevdockScenario.writeResult(result); + + assertEquals( + scenario.getJSONObject("upload") + .getJSONObject("retryOffsetRecovery") + .getInt("expectedFailureCount"), + result.getInt("simulatedFailureCount") + ); + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithRetryOffsetRecovery( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final int chunkSize = Api2DevdockScenario.fixedChunkSizeBytes(uploadConfig); + final Api2DevdockScenario.RetryOffsetRecoveryPlan plan = + Api2DevdockScenario.retryOffsetRecovery(uploadConfig); + final List recoveredOffsets = new ArrayList(); + final List requestMethods = new ArrayList(); + final int[] failureCandidateCount = new int[]{0}; + final int[] simulatedFailureCount = new int[]{0}; + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-retry-offset-recovery.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-retry-offset-recovery", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.setRequestLifecycleHooks(new TusRequestLifecycleHooks( + new TusRequestLifecycleHooks.BeforeRequest() { + @Override + public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) { + requestMethods.add(context.getMethod()); + } + }, + new TusRequestLifecycleHooks.AfterResponse() { + @Override + public void afterResponse( + TusRequestLifecycleHooks.RequestContext context + ) throws IOException { + if (plan.recoveryResponse.method.equals(context.getMethod())) { + recoveredOffsets.add(readHeaderInt( + context.getConnection(), + plan.recoveryResponse.offsetHeader + )); + } + + if (!plan.failAfterResponse.method.equals(context.getMethod())) { + return; + } + + failureCandidateCount[0] += 1; + if (failureCandidateCount[0] != plan.failAfterResponse.occurrence) { + return; + } + + simulatedFailureCount[0] += 1; + throw new IOException(plan.failAfterResponse.message); + } + } + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-retry-offset-recovery" + ); + final String[] uploadUrl = new String[]{null}; + final long[] finalOffset = new long[]{0}; + final TusExecutor executor = new TusExecutor() { + @Override + protected void makeAttempt() throws ProtocolException, IOException { + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(chunkSize); + uploader.setRequestPayloadSize(chunkSize); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + uploadUrl[0] = uploader.getUploadURL().toString(); + finalOffset[0] = uploader.getOffset(); + } + }; + executor.setDelays(new int[uploadConfig.getInt("retries")]); + if (!executor.makeAttempts()) { + throw new IOException("retry offset recovery was interrupted"); + } + + if (uploadUrl[0] == null) { + throw new IllegalStateException( + "retry offset recovery TUS upload did not expose a URL" + ); + } + assertEquals(content.length, finalOffset[0]); + assertEquals(plan.expectedFailureCount, simulatedFailureCount[0]); + assertEquals(plan.expectedRecoveryRequestCount, recoveredOffsets.size()); + assertEquals(plan.expectedRecoveredOffset, recoveredOffsets.get(0).intValue()); + assertStringList(requestMethods, plan.expectedRequestMethods); + + return new JSONObject() + .put("recoveredOffsets", new JSONArray(recoveredOffsets)) + .put("recoveryRequestCount", recoveredOffsets.size()) + .put("requestMethods", new JSONArray(requestMethods)) + .put("simulatedFailureCount", simulatedFailureCount[0]) + .put("uploadUrl", uploadUrl[0]); + } + + private static int readHeaderInt(HttpURLConnection connection, String headerName) { + final String value = connection.getHeaderField(headerName); + final int offset; + try { + offset = Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalStateException( + "retry offset recovery expected numeric " + + headerName + + " response header, got " + + value + ); + } + if (offset < 0) { + throw new IllegalStateException( + "retry offset recovery expected non-negative offset, got " + offset + ); + } + + return offset; + } + + private static void assertStringList(List actual, List expected) { + if (actual.size() != expected.size()) { + throw new IllegalStateException( + "retry offset recovery expected request methods " + + expected + + ", got " + + actual + ); + } + + for (int index = 0; index < expected.size(); index++) { + if (!actual.get(index).equals(expected.get(index))) { + throw new IllegalStateException( + "retry offset recovery expected request method " + + expected.get(index) + + " at index " + + index + + ", got " + + actual.get(index) + ); + } + } + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusRetryOffsetRecovery.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusTerminateUploadExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusTerminateUploadExampleTest.java new file mode 100644 index 0000000..07957c4 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusTerminateUploadExampleTest.java @@ -0,0 +1,166 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.net.Uri; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusRequestLifecycleHooks; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusTerminateUploadExampleTest { + @Test + public void terminatesAndroidContentUriUpload() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadAndTerminate(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertEquals(true, result.getBoolean("terminated")); + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadAndTerminate( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final Api2DevdockScenario.TerminationPlan termination = + Api2DevdockScenario.termination(uploadConfig); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final int chunkSize = Api2DevdockScenario.fixedChunkSizeBytes(uploadConfig); + final List requestMethods = new ArrayList(); + + if (termination.stopAfterAcceptedBytes > content.length) { + throw new IllegalStateException( + "terminate upload stop-after bytes " + + termination.stopAfterAcceptedBytes + + " exceeds content length " + + content.length + ); + } + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-terminate-upload.txt" + ); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.setRequestLifecycleHooks(new TusRequestLifecycleHooks( + new TusRequestLifecycleHooks.BeforeRequest() { + @Override + public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) { + requestMethods.add(context.getMethod()); + } + }, + null + )); + + final TusUploader uploader = client.createUpload(Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-terminate-upload" + )); + uploader.setChunkSize(chunkSize); + uploader.setRequestPayloadSize(termination.stopAfterAcceptedBytes); + final int uploadedChunkSize = uploader.uploadChunk(); + uploader.finish(); + + assertEquals(termination.stopAfterAcceptedBytes, uploadedChunkSize); + assertEquals(termination.stopAfterAcceptedBytes, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + final URL uploadUrl = uploader.getUploadURL(); + client.terminateUpload(uploadUrl).disconnect(); + final int verificationStatus = verifyTerminatedUpload( + client, + termination.verificationMethod, + uploadUrl + ); + assertEquals(termination.expectedVerificationStatus, verificationStatus); + + return new JSONObject() + .put("acceptedBytes", (int) uploader.getOffset()) + .put("deleteRequestCount", countMethod(requestMethods, termination.method)) + .put("requestMethods", new JSONArray(requestMethods)) + .put("terminated", true) + .put("uploadUrl", uploadUrl.toString()) + .put("verificationStatus", verificationStatus); + } + + private static int verifyTerminatedUpload( + TusClient client, + String method, + URL uploadUrl + ) throws IOException { + final HttpURLConnection connection = (HttpURLConnection) uploadUrl.openConnection(); + try { + connection.setRequestMethod(method); + client.prepareConnection(connection); + connection.connect(); + return connection.getResponseCode(); + } finally { + connection.disconnect(); + } + } + + private static int countMethod(List methods, String expectedMethod) { + int count = 0; + for (String method : methods) { + if (method.equals(expectedMethod)) { + count += 1; + } + } + + return count; + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusTerminateUpload.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadBodyHeadersExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadBodyHeadersExampleTest.java new file mode 100644 index 0000000..6a5a14e --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadBodyHeadersExampleTest.java @@ -0,0 +1,167 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusRequestLifecycleHooks; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusUploadBodyHeadersExampleTest { + @Test + public void observesAndroidTusUploadBodyHeaders() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithBodyHeaders(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithBodyHeaders( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final Map> expectedHeadersByMethod = + Api2DevdockScenario.uploadBodyHeadersByMethod(uploadConfig); + final Map> bodyHeadersByMethod = + new LinkedHashMap>(); + Api2DevdockScenario.requireFullFileChunkSize(uploadConfig); + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-upload-body-headers.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-upload-body-headers", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.setRequestLifecycleHooks(new TusRequestLifecycleHooks( + new TusRequestLifecycleHooks.BeforeRequest() { + @Override + public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) { + final Map expectedHeaders = + expectedHeadersByMethod.get(context.getMethod()); + if (expectedHeaders == null) { + return; + } + + bodyHeadersByMethod.put( + context.getMethod(), + observedBodyHeaders( + context.getConnection(), + context.getMethod(), + expectedHeaders + ) + ); + } + }, + null + )); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-upload-body-headers" + ); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(content.length); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + for (String method : expectedHeadersByMethod.keySet()) { + if (!bodyHeadersByMethod.containsKey(method)) { + throw new IllegalStateException( + "upload body headers did not observe " + method + " request" + ); + } + } + + return new JSONObject() + .put("bodyHeadersByMethod", new JSONObject(bodyHeadersByMethod)) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static Map observedBodyHeaders( + HttpURLConnection connection, + String method, + Map expectedHeaders + ) { + final Map headers = new LinkedHashMap(); + for (Map.Entry entry : expectedHeaders.entrySet()) { + final String value = connection.getRequestProperty(entry.getKey()); + if (value == null) { + throw new IllegalStateException( + "upload body headers did not observe " + entry.getKey() + " on " + method + ); + } + + headers.put(entry.getKey(), value); + } + + return headers; + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusUploadBodyHeaders.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadCallbacksExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadCallbacksExampleTest.java new file mode 100644 index 0000000..11ed16a --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadCallbacksExampleTest.java @@ -0,0 +1,185 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusUploadCallbacksExampleTest { + @Test + public void observesAndroidTusUploadCallbacks() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final JSONObject result = uploadWithCallbacks(activity, scenario, createResponse); + Api2DevdockScenario.writeResult(result); + + assertNotNull(result.getString("uploadUrl")); + } + + private static JSONObject uploadWithCallbacks( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final Api2DevdockScenario.UploadCallbacksPlan callbacks = + Api2DevdockScenario.uploadCallbacks(scenario); + final List events = new ArrayList(); + Api2DevdockScenario.requireFullFileChunkSize(uploadConfig); + + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-upload-callbacks.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-upload-callbacks", + 0 + ); + assertTrue(preferences.edit().clear().commit()); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-upload-callbacks" + ); + upload.setInputStream(new EventRecordingByteArrayInputStream(content, callbacks, events)); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + events.add(Api2DevdockScenario.uploadCallbackEventKey( + callbacks, + callbacks.eventKinds.uploadUrlAvailable + )); + uploader.setChunkSize(content.length); + uploader.setProgressListener(new TusUploader.ProgressListener() { + @Override + public void onProgress(long bytesSent, long bytesTotal) { + events.add(Api2DevdockScenario.uploadCallbackEventKey( + callbacks, + callbacks.eventKinds.progress, + Api2DevdockScenario.uploadCallbackEventKeyNumber(bytesSent), + Api2DevdockScenario.uploadCallbackEventKeyTotal(bytesTotal) + )); + } + }); + uploader.setChunkCompleteListener(new TusUploader.ChunkCompleteListener() { + @Override + public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) { + events.add(Api2DevdockScenario.uploadCallbackEventKey( + callbacks, + callbacks.eventKinds.chunkComplete, + Api2DevdockScenario.uploadCallbackEventKeyNumber(chunkSize), + Api2DevdockScenario.uploadCallbackEventKeyNumber(bytesAccepted), + Api2DevdockScenario.uploadCallbackEventKeyTotal(bytesTotal) + )); + } + }); + + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + uploader.finish(false); + events.add(Api2DevdockScenario.uploadCallbackEventKey( + callbacks, + callbacks.eventKinds.success + )); + uploader.finish(); + + final List matchedEvents = + Api2DevdockScenario.matchUploadCallbackEventKeys(callbacks, events); + assertEquals(callbacks.eventKeys, matchedEvents); + + return new JSONObject() + .put("eventKeys", new JSONArray(matchedEvents)) + .put("rawEventKeys", new JSONArray(events)) + .put("uploadUrl", uploader.getUploadURL().toString()); + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusUploadCallbacks.required")); + } + + private static final class EventRecordingByteArrayInputStream extends ByteArrayInputStream { + private final Api2DevdockScenario.UploadCallbacksPlan callbacks; + private boolean closed; + private final List events; + + EventRecordingByteArrayInputStream( + byte[] content, + Api2DevdockScenario.UploadCallbacksPlan callbacks, + List events + ) { + super(content); + this.callbacks = callbacks; + this.events = events; + } + + @Override + public void close() throws IOException { + if (!closed) { + events.add(Api2DevdockScenario.uploadCallbackEventKey( + callbacks, + callbacks.eventKinds.sourceClose + )); + closed = true; + } + super.close(); + } + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadExampleTest.java b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadExampleTest.java new file mode 100644 index 0000000..6079117 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadExampleTest.java @@ -0,0 +1,108 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +import java.io.IOException; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusUploader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class Api2DevdockTusUploadExampleTest { + @Test + public void uploadsAndroidContentUriToTransloaditAssembly() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = Api2DevdockScenario.scenarioPath(); + if (!isRequired()) { + Assume.assumeTrue( + "API2 devdock scenario is only required through the dedicated API2 QA task", + scenarioPath != null + ); + } + if (scenarioPath == null) { + throw new IllegalStateException("API2_SDK_EXAMPLE_SCENARIO must be set"); + } + + final JSONObject scenario = Api2DevdockScenario.loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final String uploadUrl = uploadWithTus(activity, scenario, createResponse); + final JSONObject result = new JSONObject(); + result.put("uploadUrl", uploadUrl); + Api2DevdockScenario.writeResult(result); + + assertNotNull(uploadUrl); + } + + private static String uploadWithTus( + Activity activity, + JSONObject scenario, + JSONObject createResponse + ) throws IOException, JSONException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig); + final Uri uri = Api2DevdockScenario.registerContentUri( + activity, + content, + "api2-devdock-upload.txt" + ); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-upload", + 0 + ); + preferences.edit().clear().commit(); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(Api2DevdockScenario.tusUrl( + uploadConfig, + scenario, + createResponse + )); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.enableRemoveFingerprintOnSuccess(); + + final TusAndroidUpload upload = Api2DevdockScenario.androidUpload( + activity, + uri, + scenario, + createResponse, + scenario.getString("scenarioId") + "-android-devdock-example" + ); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(content.length); + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); + uploader.finish(); + + assertEquals(content.length, uploader.getOffset()); + assertNotNull(uploader.getUploadURL()); + + return uploader.getUploadURL().toString(); + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusUpload.required")); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusClientConformanceScenarios.java b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusClientConformanceScenarios.java new file mode 100644 index 0000000..a1bcefb --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusClientConformanceScenarios.java @@ -0,0 +1,1342 @@ +package io.tus.android.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( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "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 GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + 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 String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "creation-with-upload", + "success", + null, + "creationWithUpload", + "creationWithUpload" + ), + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "creation-with-upload-partial-chunk", + "success", + null, + "creationWithUpload", + "creationWithUploadPartialChunk" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "chunk-complete:5:5:11", + "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 String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "creation-with-upload", + "success", + null, + "protocolVersionSelection", + "ietfDraft05CreationWithUpload" + ), + new String[] { + "createTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "upload-body-headers", + "success", + null, + "protocolVersionSelection", + "ietfDraft05ChunkedUploadComplete" + ), + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:0:11", + "progress:5:11", + "chunk-complete:5:5:11", + "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 String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "upload-body-headers", + "success", + null, + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength" + ), + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "missingInput", + "startOptionValidation", + "startValidationMissingInput" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "missingEndpointOrUploadUrl", + "startOptionValidation", + "startValidationMissingEndpointOrUploadUrl" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "unsupportedProtocol", + "startOptionValidation", + "startValidationUnsupportedProtocol" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "retryDelaysNotArray", + "startOptionValidation", + "startValidationRetryDelaysNotArray" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "parallelUploadsWithUploadUrl", + "startOptionValidation", + "startValidationParallelUploadsWithUploadUrl" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "parallelUploadsWithUploadSize", + "startOptionValidation", + "startValidationParallelUploadsWithUploadSize" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "parallelUploadsWithDeferredLength", + "startOptionValidation", + "startValidationParallelUploadsWithDeferredLength" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "parallelUploadsWithUploadDataDuringCreation", + "startOptionValidation", + "startValidationParallelUploadsWithUploadDataDuringCreation" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "parallelBoundariesWithoutParallelUploads", + "startOptionValidation", + "startValidationParallelBoundariesWithoutParallelUploads" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "start-option-validation", + "error", + "parallelBoundariesLengthMismatch", + "startOptionValidation", + "startValidationParallelBoundariesLengthMismatch" + ), + new String[0], + new String[] { + "validate-start-options", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "detailed-error", + "error", + "unexpectedCreateResponse", + "detailedErrors", + "detailedCreateResponseError" + ), + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "detailed-error", + "error", + "createUploadRequestFailed", + "detailedErrors", + "detailedCreateRequestError" + ), + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "upload-body-headers", + "success", + null, + "uploadBodyHeaders", + "uploadBodyHeaders" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "custom-request-headers", + "success", + null, + "customRequestHeaders", + "customRequestHeaders" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "request-id-headers", + "success", + null, + "requestIdHeaders", + "requestIdHeaders" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "add-request-id-header", + "apply-custom-request-headers", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "resume-from-previous-upload", + "success", + null, + "resumeUpload", + "resumeFromPreviousUpload" + ), + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + 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 String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "relative-location-resolution", + "success", + null, + "relativeLocationResolution", + "relativeLocationResolution" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "resolve-relative-location", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "array-buffer-input", + "success", + null, + "inputSources", + "arrayBufferInput" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "array-buffer-view-input", + "success", + null, + "inputSources", + "arrayBufferViewInput" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "web-readable-stream-input", + "success", + null, + "inputSources", + "webReadableStreamInput" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-web-stream", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "node-readable-stream-input", + "success", + null, + "inputSources", + "nodeReadableStreamInput" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-stream", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "node-path-input", + "success", + null, + "inputSources", + "nodePathInput" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-file", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "deferred-length-upload", + "success", + null, + "deferredLengthUpload", + "deferredLengthUpload" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + "allow-known-total-before-declaration", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "deferred-length-upload", + "success", + null, + "deferredLengthUpload", + "deferredLengthChunkedUpload" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-chunk-complete", + "emit-progress", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + "allow-known-total-before-declaration", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:0:null", + "progress:5:null", + "chunk-complete:5:5:null", + "progress:5:null", + "progress:10:null", + "chunk-complete:5:10:null", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[] { + "progress:0:11", + }, + new String[] { + "progress:5:11", + }, + new String[] { + "chunk-complete:5:5:11", + }, + new String[] { + "progress:5:11", + }, + new String[] { + "progress:10:11", + }, + new String[] { + "chunk-complete:5:10:11", + }, + new String[0], + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "override-patch-method", + "success", + null, + "overridePatchMethod", + "overridePatchMethod" + ), + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[0], + new String[0][0], + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "parallel-upload-concat", + "success", + null, + "parallelUploadConcat", + "parallelUploadConcat" + ), + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "createTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-allowed-extra-events", + null, + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[] { + "progress:", + } + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "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 GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "request-abort:3", + }, + new String[][] { + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "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 GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "request-lifecycle-hooks", + "success", + null, + "requestLifecycleHooks", + "requestLifecycleHooks" + ), + new String[] { + "getTusUploadOffset", + }, + new String[] { + "run-request-hooks", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + }, + new String[][] { + new String[0], + new String[0], + new String[0], + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "abort-upload", + "aborted", + null, + "abortUpload", + "abortUpload" + ), + new String[] { + "createTusUpload", + }, + new String[] { + "abort-current-request", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "request-abort:0", + }, + new String[][] { + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "abort-upload-after-stored-url", + "aborted", + null, + "abortUpload", + "abortUploadAfterStoredUrl" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "request-abort:1", + }, + new String[][] { + new String[0], + }, + new String[0] + ) + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenarioMetadata( + "terminate-with-retry", + "terminated", + null, + "terminateUpload", + "terminateWithRetry" + ), + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "terminate-upload", + "retry-with-backoff", + }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null, + null + ), + new String[] { + "should-retry:0:true", + "retry-schedule:0", + }, + new String[][] { + new String[0], + new String[0], + }, + new String[0] + ) + ), + }; + + private GeneratedTusClientConformanceScenarios() { + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContract.java b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContract.java new file mode 100644 index 0000000..b7290ef --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContract.java @@ -0,0 +1,1726 @@ +package io.tus.android.client; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Generated TUS protocol contract fixture used by tests. + */ +final class GeneratedTusProtocolContract { + static final Map DEFAULT_REQUEST_HEADERS = defaultRequestHeaders(); + static final Map DEFAULT_RESPONSE_HEADERS = defaultResponseHeaders(); + + 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 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[] { + new GeneratedTusResponseContract( + 201, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Location", + "location", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract( + 500, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + 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 GeneratedTusResponseContract( + 500, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + 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 GeneratedTusResponseContract( + 423, + "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 String OFFSET_DISCOVERY_OPERATION_ID = + "getTusUploadOffset"; + static final String OFFSET_DISCOVERY_METHOD = operationMethod(OFFSET_DISCOVERY_OPERATION_ID); + + 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", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + } + ), + 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", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "deferredLengthUpload", + "deferredLengthChunkedUpload", + }, + "covered-by-generated-scenario" + ), + "Create an upload without a known length and declare the length on the final upload request.", + "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 upload request reveals the total size." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Declare Upload-Length on the final upload request." + ), + }, + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-chunk-complete", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "creationWithUpload", + "creationWithUploadPartialChunk", + }, + "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", + "patchTusUpload", + }, + new String[] { + "upload-during-creation", + "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[] { + "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[] { + "requestIdHeaders", + }, + "covered-by-generated-scenario" + ), + "Add generated request IDs after protocol and custom request headers.", + "requestIdHeaders", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "add-request-id-header", + "", + "Generate a request ID and apply it after custom request headers so it is authoritative." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create uploads with a generated request ID." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes with a generated request ID." + ), + }, + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "add-request-id-header", + "apply-custom-request-headers", + } + ), + 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", + }, + new String[] { + "override-patch-method", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "parallelUploadConcat", + "parallelUploadAbortCleanup", + }, + "covered-by-generated-scenario" + ), + "Split one input into partial uploads, run the parts concurrently, clean up aborted parts, 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", + }, + new String[] { + "abort-current-request", + "concatenate-partial-uploads", + "emit-progress", + "split-parallel-upload-boundaries", + "terminate-upload", + } + ), + 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", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "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[] { + "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", + }, + new String[] { + "terminate-upload", + "retry-with-backoff", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "abortUpload", + "abortUploadAfterStoredUrl", + }, + "covered-by-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[] { + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "singleUploadLifecycle", + "creationWithUpload", + "resumeFromPreviousUpload", + }, + "covered-by-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[] { + "requestLifecycleHooks", + "retryPatchAfterOffsetRecovery", + }, + "covered-by-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[] { + "singleUploadLifecycle", + "resumeFromPreviousUpload", + }, + "covered-by-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[] { + "arrayBufferInput", + "arrayBufferViewInput", + "webReadableStreamInput", + "nodeReadableStreamInput", + "nodePathInput", + }, + "covered-by-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[] { + "webStorageUrlStorageBackend", + "fileUrlStorageBackend", + }, + "covered-by-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[] { + "ietfDraft05CreationWithUpload", + "ietfDraft05ChunkedUploadComplete", + "ietfDraft03ResumeWithoutKnownLength", + }, + "covered-by-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[] { + "relativeLocationResolution", + }, + "covered-by-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[] { + "startValidationMissingInput", + "startValidationMissingEndpointOrUploadUrl", + "startValidationUnsupportedProtocol", + "startValidationRetryDelaysNotArray", + "startValidationParallelUploadsWithUploadUrl", + "startValidationParallelUploadsWithUploadSize", + "startValidationParallelUploadsWithDeferredLength", + "startValidationParallelUploadsWithUploadDataDuringCreation", + "startValidationParallelBoundariesWithoutParallelUploads", + "startValidationParallelBoundariesLengthMismatch", + }, + "covered-by-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[] { + "detailedCreateResponseError", + "detailedCreateRequestError", + }, + "covered-by-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", + } + ), + }; + + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"absent-after-source-unavailable\",\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-while-deferred\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadRetryPolicyExhausted\",\n \"managedUploadSourceUnavailable\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"covered-by-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\",\n \"transportProfileId\": \"java-http-url-connection\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\",\n \"transportProfileId\": \"java-http-url-connection\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"kind\": \"terminal\",\n \"state\": \"succeeded\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"kind\": \"terminal\",\n \"state\": \"succeeded\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"unretryable-protocol-error\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"unretryable-protocol-error\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"retry-policy-exhausted\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"retry-policy-exhausted\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadRetryPolicyExhausted\",\n \"summary\": \"Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"source-unavailable\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"source-unavailable\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadSourceUnavailable\",\n \"summary\": \"Classify source disappearance before protocol requests as terminal without issuing a TUS request.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-while-deferred\",\n \"resumeUrl\": \"absent-while-deferred\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello later!\",\n \"fingerprint\": \"managed-network-constraint-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-network-constraint.txt\"\n },\n \"uploadPath\": \"managed-network-constraint\"\n },\n \"network\": {\n \"current\": \"metered-network\",\n \"decision\": \"defer-until-network-constraint-satisfied\",\n \"required\": \"unmetered-network\"\n },\n \"outcome\": {\n \"kind\": \"deferred\",\n \"reason\": \"network-constraint-unsatisfied\",\n \"state\": \"pending\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + + static final String[] MANAGED_UPLOAD_PRIMITIVES = + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }; + + static final String[] MANAGED_UPLOAD_RUNTIME_PROFILES = + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + }; + + static final String[] MANAGED_UPLOAD_SCENARIO_IDS = + new String[] { + "managedUploadDurableRetry", + "managedUploadPermanentFailure", + "managedUploadRetryPolicyExhausted", + "managedUploadSourceUnavailable", + "managedUploadNetworkConstraint", + }; + + static final GeneratedTusManagedUploadProofCase[] MANAGED_UPLOAD_PROOF_CASES = + new GeneratedTusManagedUploadProofCase[] { + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadDurableRetry", + new String[] { + "java", + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadPermanentFailure", + new String[] { + "java", + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadRetryPolicyExhausted", + new String[] { + "java", + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadSourceUnavailable", + new String[] { + "java", + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadNetworkConstraint", + new String[] { + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "publish-upload-state", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + }; + + static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = + GeneratedTusClientConformanceScenarios.CLIENT_CONFORMANCE_SCENARIOS; + + private GeneratedTusProtocolContract() { + } + + private static String operationMethod(String operationId) { + for (GeneratedTusProtocolOperation operation : OPERATIONS) { + if (operationId.equals(operation.operationId)) { + return operation.method; + } + } + + throw new AssertionError("Missing generated operation " + operationId); + } + + private static Map defaultRequestHeaders() { + Map result = new LinkedHashMap(); + result.put("Tus-Resumable", "1.0.0"); + return Collections.unmodifiableMap(result); + } + + private static Map defaultResponseHeaders() { + Map result = new LinkedHashMap(); + result.put("Tus-Resumable", "1.0.0"); + return Collections.unmodifiableMap(result); + } + + /** + * 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 GeneratedTusClientFeatureConformance conformance; + final String description; + final String featureId; + final GeneratedTusClientFeatureFlowStep[] flow; + final String[] operationIds; + final 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; + } + } + + /** + * Generated managed-upload feature proof fixture. + */ + static final class GeneratedTusManagedUploadProofCase { + final String featureId; + final String layer; + final String scenarioId; + final String[] proofRuntimes; + final String[] requiredPrimitives; + final String[] protocolFeatureIds; + final String[] runtimeProfiles; + + GeneratedTusManagedUploadProofCase( + String featureId, + String layer, + String scenarioId, + String[] proofRuntimes, + String[] requiredPrimitives, + String[] protocolFeatureIds, + String[] runtimeProfiles) { + this.featureId = featureId; + this.layer = layer; + this.scenarioId = scenarioId; + this.proofRuntimes = proofRuntimes; + this.requiredPrimitives = requiredPrimitives; + this.protocolFeatureIds = protocolFeatureIds; + this.runtimeProfiles = runtimeProfiles; + } + } + + /** + * Generated client conformance scenario metadata. + */ + static final class GeneratedTusClientConformanceScenarioMetadata { + final String behavior; + final String completionKind; + final String completionReason; + final String featureId; + final String scenarioId; + + GeneratedTusClientConformanceScenarioMetadata( + String behavior, + String completionKind, + String completionReason, + String featureId, + String scenarioId) { + this.behavior = behavior; + this.completionKind = completionKind; + this.completionReason = completionReason; + this.featureId = featureId; + this.scenarioId = scenarioId; + } + } + + /** + * 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 GeneratedTusClientConformanceEventPolicy eventPolicy; + final String[] eventKeys; + final String[][] eventKeyAlternativeGroups; + final String[] eventKeyExtraPrefixes; + + GeneratedTusClientConformanceScenario( + GeneratedTusClientConformanceScenarioMetadata metadata, + String[] operationIds, + String[] primitives, + GeneratedTusClientConformanceEvents events) { + this.behavior = metadata.behavior; + this.completionKind = metadata.completionKind; + this.completionReason = metadata.completionReason; + this.featureId = metadata.featureId; + this.scenarioId = metadata.scenarioId; + this.operationIds = operationIds; + this.primitives = primitives; + this.eventPolicy = events.policy; + this.eventKeys = events.keys; + this.eventKeyAlternativeGroups = events.alternativeGroups; + this.eventKeyExtraPrefixes = events.extraPrefixes; + } + } + + /** + * Generated client conformance event fixture bundle. + */ + static final class GeneratedTusClientConformanceEvents { + final GeneratedTusClientConformanceEventPolicy policy; + final String[] keys; + final String[][] alternativeGroups; + final String[] extraPrefixes; + + GeneratedTusClientConformanceEvents( + GeneratedTusClientConformanceEventPolicy policy, + String[] keys, + String[][] alternativeGroups, + String[] extraPrefixes) { + this.policy = policy; + this.keys = keys; + this.alternativeGroups = alternativeGroups; + this.extraPrefixes = extraPrefixes; + } + } + + /** + * Generated client conformance event policy fixture. + */ + static final class GeneratedTusClientConformanceEventPolicy { + final String matching; + final String deferredLengthBytesTotal; + final String progress; + final String transportProgress; + + GeneratedTusClientConformanceEventPolicy( + String matching, + String deferredLengthBytesTotal, + String progress, + String transportProgress) { + this.matching = matching; + this.deferredLengthBytesTotal = deferredLengthBytesTotal; + this.progress = progress; + this.transportProgress = transportProgress; + } + } + +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContractTest.java b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContractTest.java new file mode 100644 index 0000000..896e380 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContractTest.java @@ -0,0 +1,111 @@ +package io.tus.android.client; + +import android.app.Activity; + +import java.io.InputStream; +import java.net.URL; +import java.util.Scanner; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class GeneratedTusProtocolContractTest { + + @Test + public void shouldCoverResumeStoragePrimitive() throws Exception { + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature("singleUploadLifecycle"); + + assertContains(feature.operationIds, "createTusUpload"); + assertContains(feature.operationIds, "getTusUploadOffset"); + assertContains(feature.operationIds, "patchTusUpload"); + assertContains(feature.primitives, "store-resume-url"); + + Activity activity = Robolectric.setupActivity(Activity.class); + TusPreferencesURLStore store = new TusPreferencesURLStore( + activity.getSharedPreferences("generated-tus-contract-test", 0)); + URL url = new URL("https://tusd.tusdemo.net/files/generated"); + store.set("fingerprint", url); + + assertEquals(url, store.get("fingerprint")); + } + + @Test + public void shouldCoverManagedUploadProofCases() { + for (GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase proofCase + : GeneratedTusProtocolContract.MANAGED_UPLOAD_PROOF_CASES) { + assertEquals("managedUpload", proofCase.featureId); + assertEquals("feature-over-protocol", proofCase.layer); + assertContains( + GeneratedTusProtocolContract.MANAGED_UPLOAD_SCENARIO_IDS, + proofCase.scenarioId); + for (String primitive : proofCase.requiredPrimitives) { + assertContains(GeneratedTusProtocolContract.MANAGED_UPLOAD_PRIMITIVES, primitive); + } + for (String featureId : proofCase.protocolFeatureIds) { + findFeature(featureId); + } + } + } + + @Test + public void shouldReferenceCanonicalContractFixture() { + String contractJson = canonicalContractJson(); + + for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation + : GeneratedTusProtocolContract.OPERATIONS) { + assertCanonicalValue(contractJson, "operationId", operation.operationId); + } + for (GeneratedTusProtocolContract.GeneratedTusClientFeature feature + : GeneratedTusProtocolContract.CLIENT_FEATURES) { + assertCanonicalValue(contractJson, "featureId", feature.featureId); + } + for (GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase proofCase + : GeneratedTusProtocolContract.MANAGED_UPLOAD_PROOF_CASES) { + assertCanonicalValue(contractJson, "scenarioId", proofCase.scenarioId); + } + } + + private static String canonicalContractJson() { + InputStream input = GeneratedTusProtocolContractTest.class.getResourceAsStream( + "/api2_tus_contract.json"); + assertNotNull(input); + + try (Scanner scanner = new Scanner(input, "UTF-8").useDelimiter("\\A")) { + return scanner.hasNext() ? scanner.next() : ""; + } + } + + private static void assertCanonicalValue(String contractJson, String key, String value) { + assertTrue(contractJson.contains("\"" + key + "\": \"" + value + "\"")); + } + + 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 void assertContains(String[] values, String expected) { + for (String value : values) { + if (value.equals(expected)) { + return; + } + } + + throw new AssertionError("Missing generated value: " + expected); + } +} diff --git a/tus-android-client/src/test/java/io/tus/android/client/TestGeneratedTusManagedUploadRuntime.java b/tus-android-client/src/test/java/io/tus/android/client/TestGeneratedTusManagedUploadRuntime.java new file mode 100644 index 0000000..cf8e63a --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/TestGeneratedTusManagedUploadRuntime.java @@ -0,0 +1,1841 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.SharedPreferences; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +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 java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusExecutor; +import io.tus.java.client.TusUpload; +import io.tus.java.client.TusUploader; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests generated Android managed-upload scenarios against Android storage and Java client pieces. + */ +@RunWith(RobolectricTestRunner.class) +public class TestGeneratedTusManagedUploadRuntime { + private static final GeneratedTusManagedUploadRuntimeCase[] CASES = + new GeneratedTusManagedUploadRuntimeCase[] { + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadDurableRetry" + ), + new GeneratedTusManagedUploadRuntimeCapabilities( + true, + true, + false, + true + ), + new GeneratedTusManagedUploadRuntimePlan( + "Location", + "pending", + new String[] { + "pending", + "running", + "failed", + "running", + "succeeded", + }, + new int[] { + 0, + } + ), + new GeneratedTusManagedUploadOutcomeExpectations( + false, + false, + true, + true + ), + new GeneratedTusManagedUploadExecution( + new GeneratedTusManagedUploadTerminalExecution( + true, + false, + false + ), + new GeneratedTusManagedUploadSchedulingExecution( + false, + true + ), + new GeneratedTusManagedUploadSourceExecution( + true, + false, + -1, + false + ) + ), + new GeneratedTusManagedUploadStateExpectations( + true, + false, + false + ), + new GeneratedTusManagedUploadWorkload( + new GeneratedTusManagedUploadInput( + "hello managed!", + 7, + "managed-durable-retry-fingerprint", + "managed-durable-retry", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "running", + "failed", + new GeneratedTusManagedUploadFailure( + true, + false, + false, + "io-error", + 7 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 201, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Metadata", + "filename bWFuYWdlZC50eHQ=" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Location", + "https://tus.io/uploads/managed-durable-retry" + ), + } + ) + ), + new GeneratedTusManagedUploadRequest( + "POST", + "upload", + 7, + 204, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Content-Type", + "application/offset+octet-stream" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "0" + ), + new GeneratedTusManagedUploadHeader( + "X-HTTP-Method-Override", + "PATCH" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "7" + ), + } + ) + ), + } + ), + new GeneratedTusManagedUploadAttempt( + 1, + "running", + "succeeded", + null, + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "HEAD", + "upload", + 0, + 200, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[0] + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "7" + ), + } + ) + ), + new GeneratedTusManagedUploadRequest( + "POST", + "upload", + 7, + 204, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Content-Type", + "application/offset+octet-stream" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "7" + ), + new GeneratedTusManagedUploadHeader( + "X-HTTP-Method-Override", + "PATCH" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "14" + ), + } + ) + ), + } + ), + } + ) + ), + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadPermanentFailure" + ), + new GeneratedTusManagedUploadRuntimeCapabilities( + true, + true, + false, + true + ), + new GeneratedTusManagedUploadRuntimePlan( + "Location", + "pending", + new String[] { + "pending", + "running", + "failed", + }, + new int[0] + ), + new GeneratedTusManagedUploadOutcomeExpectations( + false, + true, + true, + false + ), + new GeneratedTusManagedUploadExecution( + new GeneratedTusManagedUploadTerminalExecution( + false, + false, + true + ), + new GeneratedTusManagedUploadSchedulingExecution( + false, + true + ), + new GeneratedTusManagedUploadSourceExecution( + true, + false, + -1, + false + ) + ), + new GeneratedTusManagedUploadStateExpectations( + true, + true, + false + ), + new GeneratedTusManagedUploadWorkload( + new GeneratedTusManagedUploadInput( + "hello failure!", + 7, + "managed-permanent-failure-fingerprint", + "managed-permanent-failure", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-permanent-failure.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "running", + "failed", + new GeneratedTusManagedUploadFailure( + false, + false, + true, + "unretryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 400, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Metadata", + "filename bWFuYWdlZC1wZXJtYW5lbnQtZmFpbHVyZS50eHQ=" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + false, + new GeneratedTusManagedUploadHeader[0] + ) + ), + } + ), + } + ) + ), + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadRetryPolicyExhausted" + ), + new GeneratedTusManagedUploadRuntimeCapabilities( + true, + true, + false, + true + ), + new GeneratedTusManagedUploadRuntimePlan( + "Location", + "pending", + new String[] { + "pending", + "running", + "failed", + "running", + "failed", + "running", + "failed", + }, + new int[] { + 0, + 0, + } + ), + new GeneratedTusManagedUploadOutcomeExpectations( + false, + true, + true, + false + ), + new GeneratedTusManagedUploadExecution( + new GeneratedTusManagedUploadTerminalExecution( + false, + true, + true + ), + new GeneratedTusManagedUploadSchedulingExecution( + false, + true + ), + new GeneratedTusManagedUploadSourceExecution( + true, + false, + -1, + false + ) + ), + new GeneratedTusManagedUploadStateExpectations( + true, + true, + false + ), + new GeneratedTusManagedUploadWorkload( + new GeneratedTusManagedUploadInput( + "hello retries!", + 7, + "managed-retry-exhausted-fingerprint", + "managed-retry-exhausted", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-retry-exhausted.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "running", + "failed", + new GeneratedTusManagedUploadFailure( + false, + false, + true, + "retryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 500, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Metadata", + "filename bWFuYWdlZC1yZXRyeS1leGhhdXN0ZWQudHh0" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[0] + ) + ), + } + ), + new GeneratedTusManagedUploadAttempt( + 1, + "running", + "failed", + new GeneratedTusManagedUploadFailure( + false, + false, + true, + "retryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 500, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Metadata", + "filename bWFuYWdlZC1yZXRyeS1leGhhdXN0ZWQudHh0" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[0] + ) + ), + } + ), + new GeneratedTusManagedUploadAttempt( + 2, + "running", + "failed", + new GeneratedTusManagedUploadFailure( + false, + false, + true, + "retryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 500, + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Metadata", + "filename bWFuYWdlZC1yZXRyeS1leGhhdXN0ZWQudHh0" + ), + } + ), + new GeneratedTusManagedUploadHeaderSet( + true, + new GeneratedTusManagedUploadHeader[0] + ) + ), + } + ), + } + ) + ), + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadSourceUnavailable" + ), + new GeneratedTusManagedUploadRuntimeCapabilities( + true, + true, + false, + true + ), + new GeneratedTusManagedUploadRuntimePlan( + "Location", + "pending", + new String[] { + "pending", + "running", + "failed", + }, + new int[0] + ), + new GeneratedTusManagedUploadOutcomeExpectations( + false, + true, + true, + false + ), + new GeneratedTusManagedUploadExecution( + new GeneratedTusManagedUploadTerminalExecution( + false, + true, + false + ), + new GeneratedTusManagedUploadSchedulingExecution( + false, + true + ), + new GeneratedTusManagedUploadSourceExecution( + false, + true, + 0, + true + ) + ), + new GeneratedTusManagedUploadStateExpectations( + false, + false, + false + ), + new GeneratedTusManagedUploadWorkload( + new GeneratedTusManagedUploadInput( + "hello missing!", + 7, + "managed-source-unavailable-fingerprint", + "managed-source-unavailable", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-source-unavailable.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "running", + "failed", + new GeneratedTusManagedUploadFailure( + false, + true, + false, + "source-unavailable", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + + } + ), + } + ) + ), + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadNetworkConstraint" + ), + new GeneratedTusManagedUploadRuntimeCapabilities( + true, + true, + false, + true + ), + new GeneratedTusManagedUploadRuntimePlan( + "Location", + "pending", + new String[] { + "pending", + }, + new int[0] + ), + new GeneratedTusManagedUploadOutcomeExpectations( + true, + false, + false, + false + ), + new GeneratedTusManagedUploadExecution( + new GeneratedTusManagedUploadTerminalExecution( + false, + false, + false + ), + new GeneratedTusManagedUploadSchedulingExecution( + true, + false + ), + new GeneratedTusManagedUploadSourceExecution( + true, + false, + -1, + false + ) + ), + new GeneratedTusManagedUploadStateExpectations( + true, + true, + false + ), + new GeneratedTusManagedUploadWorkload( + new GeneratedTusManagedUploadInput( + "hello later!", + 7, + "managed-network-constraint-fingerprint", + "managed-network-constraint", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-network-constraint.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + + } + ) + ), + }; + + /** + * Verifies Android managed uploads can persist state and resume through platform storage. + */ + @Test + public void shouldRunManagedUploadWithAndroidPlatformState() throws Exception { + for (GeneratedTusManagedUploadRuntimeCase testCase : CASES) { + Activity activity = Robolectric.setupActivity(Activity.class); + SharedPreferences stateStore = + activity.getSharedPreferences(testCase.scenarioId + "-state", 0); + SharedPreferences urlStorePreferences = + activity.getSharedPreferences(testCase.scenarioId + "-urls", 0); + assertTrue(testCase.scenarioId, stateStore.edit().clear().commit()); + assertTrue(testCase.scenarioId, urlStorePreferences.edit().clear().commit()); + + GeneratedTusManagedUploadServer server = new GeneratedTusManagedUploadServer(testCase); + server.start(); + try { + List states = new ArrayList(); + File source = writeSourceFile(testCase); + File ownedSource = ownedSourceFile(testCase, source); + recordState(testCase, states, stateStore, testCase.initialState); + + final TusPreferencesURLStore urlStore = + new TusPreferencesURLStore(urlStorePreferences); + final TusClient client = new TusClient(); + client.setUploadCreationURL(server.endpointUrlFor(testCase)); + client.enableResuming(urlStore); + client.enableRemoveFingerprintOnSuccess(); + + try { + prepareSourceBeforeProtocol(testCase, source, ownedSource, states, stateStore); + GeneratedTusAndroidScheduler scheduler = + new GeneratedTusAndroidScheduler(testCase, stateStore); + try { + if (shouldDeferBeforeProtocol(testCase)) { + scheduler.deferUntilNetworkConstraintSatisfied(); + assertDeferredResult(testCase); + } else { + TusExecutor executor = + managedExecutorFor( + testCase, + client, + ownedSource, + states, + stateStore); + Future future = scheduler.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return executor.makeAttempts(); + } + }); + assertTerminalResult(testCase, future); + } + } finally { + scheduler.shutdown(); + } + } catch (IOException error) { + if (!isSourceUnavailableBeforeProtocol(testCase)) { + throw error; + } + assertTerminalFailure(testCase, error); + } + + cleanupAfterTerminalState(testCase, ownedSource); + + assertArrayEquals( + testCase.scenarioId, + testCase.expectedStates, + states.toArray(new String[states.size()])); + assertArrayEquals( + testCase.scenarioId, + testCase.expectedStates, + storedStates(stateStore)); + assertResumeUrlState(testCase, urlStore); + assertOwnedSourceState(testCase, ownedSource); + assertInputSourceState(testCase, source); + assertProtocolRequestCount(testCase, server.requestCount()); + } finally { + server.stop(); + } + } + } + + private void assertTerminalResult( + GeneratedTusManagedUploadRuntimeCase testCase, + Future future) throws Exception { + if (!testCase.expectTerminalResult) { + throw new AssertionError(testCase.scenarioId + " expected deferred outcome"); + } + + try { + boolean result = future.get(); + if (!testCase.expectTerminalSuccess) { + throw new AssertionError(testCase.scenarioId + " expected terminal failure"); + } + assertTrue(testCase.scenarioId, result); + } catch (ExecutionException error) { + if (!testCase.expectTerminalFailure) { + throw error; + } + assertTerminalFailure(testCase, error.getCause()); + } + } + + private void assertTerminalFailure( + GeneratedTusManagedUploadRuntimeCase testCase, + Throwable error) { + if (testCase.expectProtocolExceptionOnTerminalFailure && error instanceof ProtocolException) { + assertTrue(testCase.scenarioId, error instanceof ProtocolException); + return; + } + if (testCase.expectIoExceptionOnTerminalFailure && error instanceof IOException) { + assertTrue(testCase.scenarioId, error instanceof IOException); + return; + } + + throw new AssertionError( + testCase.scenarioId + + " observed unexpected generated terminal failure " + + error); + } + + private void assertDeferredResult(GeneratedTusManagedUploadRuntimeCase testCase) { + if ( + !testCase.expectDeferredNetworkResult + || !testCase.deferBeforeProtocol + || testCase.networkConstraintSatisfied) { + throw new AssertionError(testCase.scenarioId + " expected deferred network outcome"); + } + } + + private TusExecutor managedExecutorFor( + final GeneratedTusManagedUploadRuntimeCase testCase, + final TusClient client, + final File ownedSource, + final List states, + final SharedPreferences stateStore) { + TusExecutor executor = new TusExecutor() { + private int attemptIndex; + + @Override + protected void makeAttempt() throws ProtocolException, IOException { + GeneratedTusManagedUploadAttempt attempt = testCase.attempts[attemptIndex]; + attemptIndex += 1; + recordState(testCase, states, stateStore, attempt.stateBeforeAttempt); + + try { + TusUpload upload = uploadFor(testCase, ownedSource); + TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(testCase.input.chunkSize); + uploader.setRequestPayloadSize(testCase.input.chunkSize); + while (uploader.getOffset() < upload.getSize()) { + uploader.uploadChunk(); + if ( + isAfterAcceptedOffsetFailure(attempt) + && uploader.getOffset() == attempt.failure.afterAcceptedOffset) { + uploader.finish(false); + recordState(testCase, states, stateStore, attempt.stateAfterAttempt); + throw new IOException(attempt.failure.failureMessage); + } + } + uploader.finish(); + recordState(testCase, states, stateStore, attempt.stateAfterAttempt); + } catch (ProtocolException error) { + recordDuringProtocolFailure(testCase, states, stateStore, attempt); + throw error; + } catch (IOException error) { + recordDuringProtocolFailure(testCase, states, stateStore, attempt); + throw error; + } + } + }; + executor.setDelays(testCase.retryDelays); + return executor; + } + + private boolean isAfterAcceptedOffsetFailure(GeneratedTusManagedUploadAttempt attempt) { + return attempt.failure != null + && attempt.failure.failAfterAcceptedOffset; + } + + private void recordDuringProtocolFailure( + GeneratedTusManagedUploadRuntimeCase testCase, + List states, + SharedPreferences stateStore, + GeneratedTusManagedUploadAttempt attempt) { + if (attempt.failure == null || !attempt.failure.failDuringProtocolRequest) { + return; + } + + recordState(testCase, states, stateStore, attempt.stateAfterAttempt); + } + + private TusUpload uploadFor( + GeneratedTusManagedUploadRuntimeCase testCase, + File ownedSource) throws IOException { + TusUpload upload = new TusUpload(ownedSource); + upload.setFingerprint(testCase.input.fingerprint); + upload.setMetadata(metadataFor(testCase.input.metadata)); + return upload; + } + + private Map metadataFor(GeneratedTusManagedUploadMetadata[] metadata) { + Map result = new LinkedHashMap(); + for (GeneratedTusManagedUploadMetadata entry : metadata) { + result.put(entry.name, entry.value); + } + return result; + } + + private void copyDurableSource( + GeneratedTusManagedUploadRuntimeCase testCase, + File source, + File ownedSource) throws IOException { + if (!testCase.copySourceToOwnedStorage) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated source durability capability"); + } + + copyFile(source, ownedSource); + assertTrue(testCase.scenarioId, ownedSource.exists()); + } + + private void prepareSourceBeforeProtocol( + GeneratedTusManagedUploadRuntimeCase testCase, + File source, + File ownedSource, + List states, + SharedPreferences stateStore) throws IOException { + if (testCase.prepareDurableSourceBeforeProtocol) { + copyDurableSource(testCase, source, ownedSource); + return; + } + if (testCase.simulateMissingSourceBeforeDurableCopy) { + GeneratedTusManagedUploadAttempt attempt = testCase.sourcePreparationFailureAttempt; + if (attempt == null) { + throw new AssertionError( + testCase.scenarioId + + " is missing generated source preparation failure attempt"); + } + if (source.exists() && !source.delete()) { + throw new IOException("Could not remove generated input source " + source); + } + recordState(testCase, states, stateStore, attempt.stateBeforeAttempt); + try { + copyDurableSource(testCase, source, ownedSource); + } catch (IOException error) { + recordState(testCase, states, stateStore, attempt.stateAfterAttempt); + throw error; + } + throw new AssertionError(testCase.scenarioId + " unexpectedly prepared missing source"); + } + + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated source preparation expectations"); + } + + private boolean isSourceUnavailableBeforeProtocol(GeneratedTusManagedUploadRuntimeCase testCase) { + return testCase.sourceUnavailableBeforeProtocol; + } + + private boolean shouldDeferBeforeProtocol(GeneratedTusManagedUploadRuntimeCase testCase) { + return testCase.deferBeforeProtocol; + } + + private void cleanupAfterTerminalState( + GeneratedTusManagedUploadRuntimeCase testCase, + File ownedSource) throws IOException { + if (!testCase.cleanupOwnedSourceAfterTerminalState) { + return; + } + + if (ownedSource.exists() && !ownedSource.delete()) { + throw new IOException("Could not delete generated owned source " + ownedSource); + } + } + + private void assertOwnedSourceState( + GeneratedTusManagedUploadRuntimeCase testCase, + File ownedSource) { + if (testCase.expectOwnedSourceExists) { + assertTrue(testCase.scenarioId, ownedSource.exists()); + ownedSource.delete(); + return; + } + + assertFalse(testCase.scenarioId, ownedSource.exists()); + } + + private void assertInputSourceState( + GeneratedTusManagedUploadRuntimeCase testCase, + File source) { + if (testCase.expectInputSourceExists) { + assertTrue(testCase.scenarioId, source.exists()); + source.delete(); + return; + } + + assertFalse(testCase.scenarioId, source.exists()); + } + + private void assertResumeUrlState( + GeneratedTusManagedUploadRuntimeCase testCase, + TusPreferencesURLStore urlStore) { + if (testCase.expectResumeUrlExists) { + assertTrue(testCase.scenarioId, urlStore.get(testCase.input.fingerprint) != null); + return; + } + + assertNull(testCase.scenarioId, urlStore.get(testCase.input.fingerprint)); + } + + private void assertProtocolRequestCount( + GeneratedTusManagedUploadRuntimeCase testCase, + int actualRequestCount) { + assertTrue( + testCase.scenarioId, + actualRequestCount == expectedProtocolRequestCount(testCase)); + } + + private int expectedProtocolRequestCount(GeneratedTusManagedUploadRuntimeCase testCase) { + int count = 0; + for (GeneratedTusManagedUploadAttempt attempt : testCase.attempts) { + count += attempt.requests.length; + } + return count; + } + + private void recordState( + GeneratedTusManagedUploadRuntimeCase testCase, + List states, + SharedPreferences stateStore, + String state) { + if (!testCase.usePlatformKeyValueStateBackend) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated state backend capability"); + } + + states.add(state); + SharedPreferences.Editor editor = stateStore.edit(); + editor.putInt("state-count", states.size()); + for (int index = 0; index < states.size(); index += 1) { + editor.putString("state-" + index, states.get(index)); + } + assertTrue(testCase.scenarioId, editor.commit()); + } + + private String[] storedStates(SharedPreferences stateStore) { + int count = stateStore.getInt("state-count", 0); + String[] states = new String[count]; + for (int index = 0; index < count; index += 1) { + states[index] = stateStore.getString("state-" + index, ""); + } + return states; + } + + private File writeSourceFile(GeneratedTusManagedUploadRuntimeCase testCase) throws IOException { + File source = File.createTempFile(testCase.scenarioId, "-source.bin"); + FileOutputStream output = new FileOutputStream(source); + try { + output.write(testCase.input.content.getBytes(StandardCharsets.UTF_8)); + } finally { + output.close(); + } + return source; + } + + private File ownedSourceFile( + GeneratedTusManagedUploadRuntimeCase testCase, + File source) { + return new File(source.getParentFile(), testCase.scenarioId + "-android-owned.bin"); + } + + private void copyFile(File source, File destination) throws IOException { + FileInputStream input = new FileInputStream(source); + try { + FileOutputStream output = new FileOutputStream(destination); + try { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } finally { + output.close(); + } + } finally { + input.close(); + } + } + + private static String offsetDiscoveryMethod() { + return GeneratedTusProtocolContract.OFFSET_DISCOVERY_METHOD; + } + + private static final class GeneratedTusAndroidScheduler { + private final ExecutorService worker = Executors.newSingleThreadExecutor(); + private final GeneratedTusManagedUploadRuntimeCase testCase; + private final SharedPreferences stateStore; + + GeneratedTusAndroidScheduler( + GeneratedTusManagedUploadRuntimeCase testCase, + SharedPreferences stateStore) { + this.testCase = testCase; + this.stateStore = stateStore; + } + + Future submit(Callable work) { + if (!testCase.useDurableOsScheduler) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated scheduler capability"); + } + + assertTrue( + testCase.scenarioId, + stateStore.edit() + .putBoolean("durable-scheduler", testCase.useDurableOsScheduler) + .commit()); + return worker.submit(work); + } + + void deferUntilNetworkConstraintSatisfied() { + if (!testCase.useDurableOsScheduler) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated scheduler capability"); + } + if ( + !testCase.deferBeforeProtocol + || testCase.networkConstraintSatisfied) { + throw new AssertionError(testCase.scenarioId + " expected unsatisfied network"); + } + + assertTrue( + testCase.scenarioId, + stateStore.edit() + .putBoolean("durable-scheduler", testCase.useDurableOsScheduler) + .putBoolean("network-satisfied", testCase.networkConstraintSatisfied) + .commit()); + } + + void shutdown() { + worker.shutdownNow(); + } + } + + private static final class GeneratedTusManagedUploadServer { + private final ServerSocket serverSocket; + private final GeneratedTusManagedUploadRuntimeCase testCase; + private volatile int requestCount; + private volatile boolean running; + private Thread thread; + + GeneratedTusManagedUploadServer(GeneratedTusManagedUploadRuntimeCase testCase) + throws IOException { + this.testCase = testCase; + this.serverSocket = new ServerSocket(0); + } + + void start() { + running = true; + thread = new Thread(new Runnable() { + @Override + public void run() { + serve(); + } + }); + thread.start(); + } + + void stop() throws IOException, InterruptedException { + running = false; + serverSocket.close(); + if (thread != null) { + thread.join(1000); + } + } + + URL endpointUrlFor(GeneratedTusManagedUploadRuntimeCase testCase) throws IOException { + return new URL("http://127.0.0.1:" + serverSocket.getLocalPort() + "/files"); + } + + URL uploadUrlFor(GeneratedTusManagedUploadRuntimeCase testCase) throws IOException { + return new URL(endpointUrlFor(testCase).toString() + "/" + testCase.input.uploadPath); + } + + int requestCount() { + return requestCount; + } + + private void serve() { + while (running) { + try { + Socket socket = serverSocket.accept(); + handle(socket); + } catch (SocketException error) { + if (running) { + throw new AssertionError(error); + } + } catch (IOException error) { + throw new AssertionError(error); + } + } + } + + private void handle(Socket socket) throws IOException { + try { + GeneratedTusHttpRequest httpRequest = + readHttpRequest(socket.getInputStream(), socket.getOutputStream()); + requestCount += 1; + GeneratedTusManagedUploadRequest request = findRequest(httpRequest); + if (request == null) { + respondNotFound(socket.getOutputStream()); + return; + } + + respond(socket.getOutputStream(), request); + } finally { + socket.close(); + } + } + + private GeneratedTusManagedUploadRequest findRequest(GeneratedTusHttpRequest httpRequest) + throws IOException { + for (GeneratedTusManagedUploadAttempt attempt : testCase.attempts) { + for (GeneratedTusManagedUploadRequest request : attempt.requests) { + if (matchesRequest(httpRequest, request)) { + return request; + } + } + } + + return null; + } + + private boolean matchesRequest( + GeneratedTusHttpRequest httpRequest, + GeneratedTusManagedUploadRequest request) throws IOException { + if (!pathFor(request).equals(httpRequest.path)) { + return false; + } + if (httpRequest.bodySize != request.bodySize) { + return false; + } + if (!methodMatches(httpRequest, request)) { + return false; + } + if (!headersMatch( + httpRequest.headers, + request.requestHeaders, + GeneratedTusProtocolContract.DEFAULT_REQUEST_HEADERS)) { + return false; + } + + return true; + } + + private boolean headersMatch( + Map> actualHeaders, + GeneratedTusManagedUploadHeaderSet expectedHeaders, + Map defaultHeaders) { + if (expectedHeaders.includesDefaultProtocolHeaders) { + for (Map.Entry entry : defaultHeaders.entrySet()) { + if (!entry.getValue().equals(headerValue(actualHeaders, entry.getKey()))) { + return false; + } + } + } + for (GeneratedTusManagedUploadHeader header : expectedHeaders.headers) { + if (!header.value.equals(headerValue(actualHeaders, header.name))) { + return false; + } + } + + return true; + } + + private boolean methodMatches( + GeneratedTusHttpRequest httpRequest, + GeneratedTusManagedUploadRequest request) { + return request.method.equals(httpRequest.method); + } + + private String pathFor(GeneratedTusManagedUploadRequest request) throws IOException { + if ("endpoint".equals(request.url)) { + return endpointUrlFor(testCase).getPath(); + } + + return uploadUrlFor(testCase).getPath(); + } + + private String responseHeaderValueFor(GeneratedTusManagedUploadHeader header) + throws IOException { + if (!testCase.locationHeaderName.equals(header.name)) { + return header.value; + } + + return uploadUrlFor(testCase).toString(); + } + + private void respond(OutputStream output, GeneratedTusManagedUploadRequest request) + throws IOException { + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 ").append(request.statusCode).append(" Generated\r\n"); + if (request.responseHeaders.includesDefaultProtocolHeaders) { + for (Map.Entry entry + : GeneratedTusProtocolContract.DEFAULT_RESPONSE_HEADERS.entrySet()) { + appendHeader(response, entry.getKey(), entry.getValue()); + } + } + for (GeneratedTusManagedUploadHeader header : request.responseHeaders.headers) { + appendHeader(response, header.name, responseHeaderValueFor(header)); + } + response.append("Content-Length: 0\r\n"); + response.append("Connection: close\r\n"); + response.append("\r\n"); + output.write(response.toString().getBytes(StandardCharsets.UTF_8)); + } + + private static void appendHeader(StringBuilder response, String name, String value) { + response.append(name) + .append(": ") + .append(value) + .append("\r\n"); + } + + private GeneratedTusHttpRequest readHttpRequest(InputStream input, OutputStream output) + throws IOException { + ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(); + int previousThird = -1; + int previousSecond = -1; + int previousFirst = -1; + int current; + while ((current = input.read()) != -1) { + headerBytes.write(current); + if ( + previousThird == '\r' + && previousSecond == '\n' + && previousFirst == '\r' + && current == '\n') { + break; + } + previousThird = previousSecond; + previousSecond = previousFirst; + previousFirst = current; + } + + String headerText = headerBytes.toString(StandardCharsets.UTF_8.name()); + String[] lines = headerText.split("\\r\\n"); + String[] requestLine = lines[0].split(" "); + Map> headers = new LinkedHashMap>(); + for (int index = 1; index < lines.length; index += 1) { + String line = lines[index]; + if (line.length() == 0) { + continue; + } + int separator = line.indexOf(":"); + if (separator < 0) { + continue; + } + String name = line.substring(0, separator); + String value = line.substring(separator + 1).trim(); + List values = headers.get(name); + if (values == null) { + values = new ArrayList(); + headers.put(name, values); + } + values.add(value); + } + + if ("100-continue".equalsIgnoreCase(headerValue(headers, "Expect"))) { + output.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(StandardCharsets.UTF_8)); + output.flush(); + } + + int bodySize = drainRequestBody(input, headers); + return new GeneratedTusHttpRequest(requestLine[0], requestLine[1], headers, bodySize); + } + + private static int contentLength(Map> headers) { + String header = headerValue(headers, "Content-Length"); + if (header == null || header.length() == 0) { + return 0; + } + + return Integer.parseInt(header); + } + + private static int drainRequestBody(InputStream input, Map> headers) + throws IOException { + if ("chunked".equalsIgnoreCase(headerValue(headers, "Transfer-Encoding"))) { + return drainChunkedRequestBody(input); + } + + return drainFixedRequestBody(input, contentLength(headers)); + } + + private static int drainFixedRequestBody(InputStream input, int contentLength) + throws IOException { + int remaining = contentLength; + byte[] buffer = new byte[8192]; + while (remaining > 0) { + int read = input.read(buffer, 0, Math.min(buffer.length, remaining)); + if (read == -1) { + break; + } + remaining -= read; + } + return contentLength - remaining; + } + + private static int drainChunkedRequestBody(InputStream input) throws IOException { + int bodySize = 0; + while (true) { + String line = readAsciiLine(input); + int extensionIndex = line.indexOf(";"); + String sizeText = extensionIndex < 0 ? line : line.substring(0, extensionIndex); + int chunkSize = Integer.parseInt(sizeText.trim(), 16); + if (chunkSize == 0) { + drainChunkedTrailers(input); + return bodySize; + } + + bodySize += drainFixedRequestBody(input, chunkSize); + readAsciiLine(input); + } + } + + private static void drainChunkedTrailers(InputStream input) throws IOException { + while (true) { + String line = readAsciiLine(input); + if (line.length() == 0) { + return; + } + } + } + + private static String readAsciiLine(InputStream input) throws IOException { + ByteArrayOutputStream line = new ByteArrayOutputStream(); + int current; + while ((current = input.read()) != -1) { + if (current == '\n') { + break; + } + if (current != '\r') { + line.write(current); + } + } + return line.toString(StandardCharsets.UTF_8.name()); + } + + private static void respondNotFound(OutputStream output) throws IOException { + byte[] body = "No generated request matched".getBytes(StandardCharsets.UTF_8); + output.write("HTTP/1.1 404 Generated\r\n".getBytes(StandardCharsets.UTF_8)); + output.write(("Content-Length: " + body.length + "\r\n").getBytes(StandardCharsets.UTF_8)); + output.write("Connection: close\r\n\r\n".getBytes(StandardCharsets.UTF_8)); + output.write(body); + } + + private static String headerValue(Map> headers, String name) { + for (Map.Entry> entry : headers.entrySet()) { + if (!entry.getKey().equalsIgnoreCase(name) || entry.getValue().isEmpty()) { + continue; + } + + return entry.getValue().get(0); + } + + return null; + } + + private static final class GeneratedTusHttpRequest { + final String method; + final String path; + final Map> headers; + final int bodySize; + + GeneratedTusHttpRequest( + String method, + String path, + Map> headers, + int bodySize) { + this.method = method; + this.path = path; + this.headers = headers; + this.bodySize = bodySize; + } + } + } + + private static final class GeneratedTusManagedUploadRuntimeCase { + final String scenarioId; + final boolean copySourceToOwnedStorage; + final boolean useDurableOsScheduler; + final boolean useFilesystemStateBackend; + final boolean usePlatformKeyValueStateBackend; + final String initialState; + final String locationHeaderName; + final boolean expectDeferredNetworkResult; + final boolean expectTerminalFailure; + final boolean expectTerminalResult; + final boolean expectTerminalSuccess; + final boolean cleanupOwnedSourceAfterTerminalState; + final boolean deferBeforeProtocol; + final boolean expectIoExceptionOnTerminalFailure; + final boolean expectProtocolExceptionOnTerminalFailure; + final boolean networkConstraintSatisfied; + final boolean prepareDurableSourceBeforeProtocol; + final boolean simulateMissingSourceBeforeDurableCopy; + final boolean sourceUnavailableBeforeProtocol; + final boolean expectInputSourceExists; + final boolean expectOwnedSourceExists; + final boolean expectResumeUrlExists; + final String[] expectedStates; + final int[] retryDelays; + final String offsetDiscoveryMethod; + final GeneratedTusManagedUploadInput input; + final GeneratedTusManagedUploadAttempt[] attempts; + final GeneratedTusManagedUploadAttempt sourcePreparationFailureAttempt; + + GeneratedTusManagedUploadRuntimeCase( + GeneratedTusManagedUploadRuntimeProfile profile, + GeneratedTusManagedUploadRuntimeCapabilities runtimeCapabilities, + GeneratedTusManagedUploadRuntimePlan runtimePlan, + GeneratedTusManagedUploadOutcomeExpectations outcomeExpectations, + GeneratedTusManagedUploadExecution execution, + GeneratedTusManagedUploadStateExpectations stateExpectations, + GeneratedTusManagedUploadWorkload workload) { + this.scenarioId = profile.scenarioId; + this.copySourceToOwnedStorage = runtimeCapabilities.copySourceToOwnedStorage; + this.useDurableOsScheduler = runtimeCapabilities.useDurableOsScheduler; + this.useFilesystemStateBackend = runtimeCapabilities.useFilesystemStateBackend; + this.usePlatformKeyValueStateBackend = + runtimeCapabilities.usePlatformKeyValueStateBackend; + this.initialState = runtimePlan.initialState; + this.locationHeaderName = runtimePlan.locationHeaderName; + this.expectDeferredNetworkResult = outcomeExpectations.expectDeferredNetworkResult; + this.expectTerminalFailure = outcomeExpectations.expectTerminalFailure; + this.expectTerminalResult = outcomeExpectations.expectTerminalResult; + this.expectTerminalSuccess = outcomeExpectations.expectTerminalSuccess; + this.cleanupOwnedSourceAfterTerminalState = execution.cleanupOwnedSourceAfterTerminalState; + this.deferBeforeProtocol = execution.deferBeforeProtocol; + this.expectIoExceptionOnTerminalFailure = execution.expectIoExceptionOnTerminalFailure; + this.expectProtocolExceptionOnTerminalFailure = execution.expectProtocolExceptionOnTerminalFailure; + this.networkConstraintSatisfied = execution.networkConstraintSatisfied; + this.prepareDurableSourceBeforeProtocol = execution.prepareDurableSourceBeforeProtocol; + this.simulateMissingSourceBeforeDurableCopy = execution.simulateMissingSourceBeforeDurableCopy; + this.sourceUnavailableBeforeProtocol = execution.sourceUnavailableBeforeProtocol; + this.expectInputSourceExists = stateExpectations.inputSourceExists; + this.expectOwnedSourceExists = stateExpectations.ownedSourceExists; + this.expectResumeUrlExists = stateExpectations.resumeUrlExists; + this.expectedStates = runtimePlan.expectedStates; + this.retryDelays = runtimePlan.retryDelays; + this.offsetDiscoveryMethod = offsetDiscoveryMethod(); + this.input = workload.input; + this.attempts = workload.attempts; + this.sourcePreparationFailureAttempt = + execution.sourcePreparationFailureAttemptIndex < 0 + ? null + : workload.attempts[execution.sourcePreparationFailureAttemptIndex]; + } + } + + private static final class GeneratedTusManagedUploadOutcomeExpectations { + final boolean expectDeferredNetworkResult; + final boolean expectTerminalFailure; + final boolean expectTerminalResult; + final boolean expectTerminalSuccess; + + GeneratedTusManagedUploadOutcomeExpectations( + boolean expectDeferredNetworkResult, + boolean expectTerminalFailure, + boolean expectTerminalResult, + boolean expectTerminalSuccess) { + this.expectDeferredNetworkResult = expectDeferredNetworkResult; + this.expectTerminalFailure = expectTerminalFailure; + this.expectTerminalResult = expectTerminalResult; + this.expectTerminalSuccess = expectTerminalSuccess; + } + } + + private static final class GeneratedTusManagedUploadRuntimeProfile { + final String scenarioId; + + GeneratedTusManagedUploadRuntimeProfile(String scenarioId) { + this.scenarioId = scenarioId; + } + } + + private static final class GeneratedTusManagedUploadRuntimeCapabilities { + final boolean copySourceToOwnedStorage; + final boolean useDurableOsScheduler; + final boolean useFilesystemStateBackend; + final boolean usePlatformKeyValueStateBackend; + + GeneratedTusManagedUploadRuntimeCapabilities( + boolean copySourceToOwnedStorage, + boolean useDurableOsScheduler, + boolean useFilesystemStateBackend, + boolean usePlatformKeyValueStateBackend) { + this.copySourceToOwnedStorage = copySourceToOwnedStorage; + this.useDurableOsScheduler = useDurableOsScheduler; + this.useFilesystemStateBackend = useFilesystemStateBackend; + this.usePlatformKeyValueStateBackend = usePlatformKeyValueStateBackend; + } + } + + private static final class GeneratedTusManagedUploadRuntimePlan { + final String[] expectedStates; + final String initialState; + final String locationHeaderName; + final int[] retryDelays; + + GeneratedTusManagedUploadRuntimePlan( + String locationHeaderName, + String initialState, + String[] expectedStates, + int[] retryDelays) { + this.expectedStates = expectedStates; + this.initialState = initialState; + this.locationHeaderName = locationHeaderName; + this.retryDelays = retryDelays; + } + } + + private static final class GeneratedTusManagedUploadExecution { + final boolean cleanupOwnedSourceAfterTerminalState; + final boolean deferBeforeProtocol; + final boolean expectIoExceptionOnTerminalFailure; + final boolean expectProtocolExceptionOnTerminalFailure; + final boolean networkConstraintSatisfied; + final boolean prepareDurableSourceBeforeProtocol; + final boolean simulateMissingSourceBeforeDurableCopy; + final int sourcePreparationFailureAttemptIndex; + final boolean sourceUnavailableBeforeProtocol; + + GeneratedTusManagedUploadExecution( + GeneratedTusManagedUploadTerminalExecution terminalExecution, + GeneratedTusManagedUploadSchedulingExecution schedulingExecution, + GeneratedTusManagedUploadSourceExecution sourceExecution) { + this.cleanupOwnedSourceAfterTerminalState = + terminalExecution.cleanupOwnedSourceAfterTerminalState; + this.deferBeforeProtocol = schedulingExecution.deferBeforeProtocol; + this.expectIoExceptionOnTerminalFailure = + terminalExecution.expectIoExceptionOnTerminalFailure; + this.expectProtocolExceptionOnTerminalFailure = + terminalExecution.expectProtocolExceptionOnTerminalFailure; + this.networkConstraintSatisfied = schedulingExecution.networkConstraintSatisfied; + this.prepareDurableSourceBeforeProtocol = + sourceExecution.prepareDurableSourceBeforeProtocol; + this.simulateMissingSourceBeforeDurableCopy = + sourceExecution.simulateMissingSourceBeforeDurableCopy; + this.sourcePreparationFailureAttemptIndex = + sourceExecution.sourcePreparationFailureAttemptIndex; + this.sourceUnavailableBeforeProtocol = sourceExecution.sourceUnavailableBeforeProtocol; + } + } + + private static final class GeneratedTusManagedUploadTerminalExecution { + final boolean cleanupOwnedSourceAfterTerminalState; + final boolean expectIoExceptionOnTerminalFailure; + final boolean expectProtocolExceptionOnTerminalFailure; + + GeneratedTusManagedUploadTerminalExecution( + boolean cleanupOwnedSourceAfterTerminalState, + boolean expectIoExceptionOnTerminalFailure, + boolean expectProtocolExceptionOnTerminalFailure) { + this.cleanupOwnedSourceAfterTerminalState = cleanupOwnedSourceAfterTerminalState; + this.expectIoExceptionOnTerminalFailure = expectIoExceptionOnTerminalFailure; + this.expectProtocolExceptionOnTerminalFailure = expectProtocolExceptionOnTerminalFailure; + } + } + + private static final class GeneratedTusManagedUploadSchedulingExecution { + final boolean deferBeforeProtocol; + final boolean networkConstraintSatisfied; + + GeneratedTusManagedUploadSchedulingExecution( + boolean deferBeforeProtocol, + boolean networkConstraintSatisfied) { + this.deferBeforeProtocol = deferBeforeProtocol; + this.networkConstraintSatisfied = networkConstraintSatisfied; + } + } + + private static final class GeneratedTusManagedUploadSourceExecution { + final boolean prepareDurableSourceBeforeProtocol; + final boolean simulateMissingSourceBeforeDurableCopy; + final int sourcePreparationFailureAttemptIndex; + final boolean sourceUnavailableBeforeProtocol; + + GeneratedTusManagedUploadSourceExecution( + boolean prepareDurableSourceBeforeProtocol, + boolean simulateMissingSourceBeforeDurableCopy, + int sourcePreparationFailureAttemptIndex, + boolean sourceUnavailableBeforeProtocol) { + this.prepareDurableSourceBeforeProtocol = prepareDurableSourceBeforeProtocol; + this.simulateMissingSourceBeforeDurableCopy = simulateMissingSourceBeforeDurableCopy; + this.sourcePreparationFailureAttemptIndex = sourcePreparationFailureAttemptIndex; + this.sourceUnavailableBeforeProtocol = sourceUnavailableBeforeProtocol; + } + } + + private static final class GeneratedTusManagedUploadStateExpectations { + final boolean inputSourceExists; + final boolean ownedSourceExists; + final boolean resumeUrlExists; + + GeneratedTusManagedUploadStateExpectations( + boolean inputSourceExists, + boolean ownedSourceExists, + boolean resumeUrlExists) { + this.inputSourceExists = inputSourceExists; + this.ownedSourceExists = ownedSourceExists; + this.resumeUrlExists = resumeUrlExists; + } + } + + private static final class GeneratedTusManagedUploadInput { + final String content; + final int chunkSize; + final String fingerprint; + final String uploadPath; + final GeneratedTusManagedUploadMetadata[] metadata; + + GeneratedTusManagedUploadInput( + String content, + int chunkSize, + String fingerprint, + String uploadPath, + GeneratedTusManagedUploadMetadata[] metadata) { + this.content = content; + this.chunkSize = chunkSize; + this.fingerprint = fingerprint; + this.uploadPath = uploadPath; + this.metadata = metadata; + } + } + + private static final class GeneratedTusManagedUploadWorkload { + final GeneratedTusManagedUploadAttempt[] attempts; + final GeneratedTusManagedUploadInput input; + + GeneratedTusManagedUploadWorkload( + GeneratedTusManagedUploadInput input, + GeneratedTusManagedUploadAttempt[] attempts) { + this.attempts = attempts; + this.input = input; + } + } + + private static final class GeneratedTusManagedUploadAttempt { + final int attemptIndex; + final String stateAfterAttempt; + final String stateBeforeAttempt; + final GeneratedTusManagedUploadFailure failure; + final GeneratedTusManagedUploadRequest[] requests; + + GeneratedTusManagedUploadAttempt( + int attemptIndex, + String stateBeforeAttempt, + String stateAfterAttempt, + GeneratedTusManagedUploadFailure failure, + GeneratedTusManagedUploadRequest[] requests) { + this.attemptIndex = attemptIndex; + this.stateAfterAttempt = stateAfterAttempt; + this.stateBeforeAttempt = stateBeforeAttempt; + this.failure = failure; + this.requests = requests; + } + } + + private static final class GeneratedTusManagedUploadFailure { + final long afterAcceptedOffset; + final boolean failAfterAcceptedOffset; + final boolean failBeforeProtocolRequest; + final boolean failDuringProtocolRequest; + final String failureMessage; + + GeneratedTusManagedUploadFailure( + boolean failAfterAcceptedOffset, + boolean failBeforeProtocolRequest, + boolean failDuringProtocolRequest, + String failureMessage, + long afterAcceptedOffset) { + this.afterAcceptedOffset = afterAcceptedOffset; + this.failAfterAcceptedOffset = failAfterAcceptedOffset; + this.failBeforeProtocolRequest = failBeforeProtocolRequest; + this.failDuringProtocolRequest = failDuringProtocolRequest; + this.failureMessage = failureMessage; + } + } + + private static final class GeneratedTusManagedUploadRequest { + final String method; + final String url; + final int bodySize; + final int statusCode; + final GeneratedTusManagedUploadHeaderSet requestHeaders; + final GeneratedTusManagedUploadHeaderSet responseHeaders; + + GeneratedTusManagedUploadRequest( + String method, + String url, + int bodySize, + int statusCode, + GeneratedTusManagedUploadHeaderSet requestHeaders, + GeneratedTusManagedUploadHeaderSet responseHeaders) { + this.method = method; + this.url = url; + this.bodySize = bodySize; + this.statusCode = statusCode; + this.requestHeaders = requestHeaders; + this.responseHeaders = responseHeaders; + } + } + + private static final class GeneratedTusManagedUploadHeaderSet { + final boolean includesDefaultProtocolHeaders; + final GeneratedTusManagedUploadHeader[] headers; + + GeneratedTusManagedUploadHeaderSet( + boolean includesDefaultProtocolHeaders, + GeneratedTusManagedUploadHeader[] headers) { + this.includesDefaultProtocolHeaders = includesDefaultProtocolHeaders; + this.headers = headers; + } + } + + private static final class GeneratedTusManagedUploadHeader { + final String name; + final String value; + + GeneratedTusManagedUploadHeader(String name, String value) { + this.name = name; + this.value = value; + } + } + + private static final class GeneratedTusManagedUploadMetadata { + final String name; + final String value; + + GeneratedTusManagedUploadMetadata(String name, String value) { + this.name = name; + this.value = value; + } + } + +} diff --git a/tus-android-client/src/test/resources/api2_tus_contract.json b/tus-android-client/src/test/resources/api2_tus_contract.json new file mode 100644 index 0000000..6cb387d --- /dev/null +++ b/tus-android-client/src/test/resources/api2_tus_contract.json @@ -0,0 +1,8763 @@ +{ + "clientConformanceScenarios": [ + { + "behavior": "single-upload-lifecycle", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/generated-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "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" + ], + "eventKinds": [ + "fingerprint", + "upload-url-available", + "url-storage-add", + "progress", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "singleUploadLifecycle", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/generated-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/generated-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/generated-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": "contract-single-fingerprint" + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": true, + "storedUpload": null + } + }, + "scenarioId": "singleUploadLifecycle" + }, + { + "behavior": "creation-with-upload", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/creation-with-upload-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close" + ], + "eventKinds": [ + "progress", + "upload-url-available", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "creationWithUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "uploadDataDuringCreation", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload" + ], + "primitives": [ + "upload-during-creation", + "emit-progress" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/creation-with-upload-contract", + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/creation-with-upload-contract", + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "creationWithUpload" + }, + { + "behavior": "creation-with-upload-partial-chunk", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/creation-with-upload-partial-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "progress:0:11", + "progress:5:11", + "upload-url-available", + "chunk-complete:5:5:11", + "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" + ], + "eventKinds": [ + "progress", + "upload-url-available", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "creationWithUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 5 + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "uploadDataDuringCreation", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "upload-during-creation", + "emit-progress" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/creation-with-upload-partial-contract", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/creation-with-upload-partial-contract", + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "10" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "10", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": "https://tus.io/uploads/creation-with-upload-partial-contract", + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/creation-with-upload-partial-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 1, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "10" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-final-chunk", + "uploadUrl": "https://tus.io/uploads/creation-with-upload-partial-contract", + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "10" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/creation-with-upload-partial-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "creationWithUploadPartialChunk" + }, + { + "behavior": "creation-with-upload", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/ietf-draft-05-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close" + ], + "eventKinds": [ + "progress", + "upload-url-available", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "protocolVersionSelection", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "protocol", + "value": "ietf-draft-05" + }, + { + "key": "uploadDataDuringCreation", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload" + ], + "primitives": [ + "select-client-protocol" + ], + "requests": [ + { + "absentHeaders": [ + "Tus-Resumable" + ], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": { + "Upload-Length": "11", + "Upload-Complete": "?1", + "Content-Type": "application/partial-upload", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Location": "https://tus.io/uploads/ietf-draft-05-contract", + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/ietf-draft-05-contract", + "Upload-Offset": "11" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "6", + "Upload-Length": "11", + "Upload-Complete": "?1", + "Content-Type": "application/partial-upload", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "ietfDraft05CreationWithUpload" + }, + { + "behavior": "upload-body-headers", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/ietf-draft-05-chunked-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "upload-url-available", + "progress:0:11", + "progress:5:11", + "chunk-complete:5:5:11", + "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" + ], + "eventKinds": [ + "upload-url-available", + "progress", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "protocolVersionSelection", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 5 + }, + { + "key": "protocol", + "value": "ietf-draft-05" + }, + { + "key": "uploadUrl", + "value": "https://tus.io/uploads/ietf-draft-05-chunked-contract" + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "select-client-protocol" + ], + "requests": [ + { + "absentHeaders": [ + "Tus-Resumable" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Upload-Length": "11", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Length": "11", + "Upload-Offset": "0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 0, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "6" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/ietf-draft-05-chunked-contract" + }, + { + "absentHeaders": [ + "Tus-Resumable" + ], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": { + "Upload-Complete": "?0", + "Content-Type": "application/partial-upload", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "5" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "6", + "Upload-Complete": "?0", + "Content-Type": "application/partial-upload", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/ietf-draft-05-chunked-contract" + }, + { + "absentHeaders": [ + "Tus-Resumable" + ], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": { + "Upload-Complete": "?0", + "Content-Type": "application/partial-upload", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Upload-Offset": "10" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "10" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "6", + "Upload-Complete": "?0", + "Content-Type": "application/partial-upload", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/ietf-draft-05-chunked-contract" + }, + { + "absentHeaders": [ + "Tus-Resumable" + ], + "abort": false, + "bodySize": 1, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": { + "Upload-Complete": "?1", + "Content-Type": "application/partial-upload", + "Upload-Offset": "10" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11" + } + }, + "role": "upload-final-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 3, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "6", + "Upload-Complete": "?1", + "Content-Type": "application/partial-upload", + "Upload-Offset": "10" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/ietf-draft-05-chunked-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "ietfDraft05ChunkedUploadComplete" + }, + { + "behavior": "upload-body-headers", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/ietf-draft-03-resume-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close" + ], + "eventKinds": [ + "upload-url-available", + "progress", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "protocolVersionSelection", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 6 + }, + { + "key": "protocol", + "value": "ietf-draft-03" + }, + { + "key": "uploadUrl", + "value": "https://tus.io/uploads/ietf-draft-03-resume-contract" + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "select-client-protocol" + ], + "requests": [ + { + "absentHeaders": [ + "Tus-Resumable" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Offset": "5" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 0, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "5" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/ietf-draft-03-resume-contract" + }, + { + "absentHeaders": [ + "Content-Type", + "Tus-Resumable" + ], + "abort": false, + "bodySize": 6, + "bodyStart": null, + "errorMessage": null, + "headerMode": "exact", + "headers": { + "Upload-Complete": "?1", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": "exact", + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Upload-Draft-Interop-Version": "5", + "Upload-Complete": "?1", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/ietf-draft-03-resume-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "ietfDraft03ResumeWithoutKnownLength" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: no file or stream to upload provided", + "completionReason": "missingInput", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + } + ], + "inputSource": { + "content": "", + "kind": "none" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationMissingInput" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: neither an endpoint or an upload URL is provided", + "completionReason": "missingEndpointOrUploadUrl", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationMissingEndpointOrUploadUrl" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: unsupported protocol tus-v9", + "completionReason": "unsupportedProtocol", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "protocol", + "value": "tus-v9" + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationUnsupportedProtocol" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: the `retryDelays` option must either be an array or null", + "completionReason": "retryDelaysNotArray", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "rawOptions", + "value": { + "retryDelays": 44 + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationRetryDelaysNotArray" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: cannot use the `uploadUrl` option when parallelUploads is enabled", + "completionReason": "parallelUploadsWithUploadUrl", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "parallelUploads", + "value": 2 + }, + { + "key": "uploadUrl", + "value": "https://tus.io/uploads/start-validation-upload-url" + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationParallelUploadsWithUploadUrl" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: cannot use the `uploadSize` option when parallelUploads is enabled", + "completionReason": "parallelUploadsWithUploadSize", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "parallelUploads", + "value": 2 + }, + { + "key": "uploadSize", + "value": 11 + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationParallelUploadsWithUploadSize" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: cannot use the `uploadLengthDeferred` option when parallelUploads is enabled", + "completionReason": "parallelUploadsWithDeferredLength", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "parallelUploads", + "value": 2 + }, + { + "key": "uploadLengthDeferred", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationParallelUploadsWithDeferredLength" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: cannot use the `uploadDataDuringCreation` option when parallelUploads is enabled", + "completionReason": "parallelUploadsWithUploadDataDuringCreation", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "parallelUploads", + "value": 2 + }, + { + "key": "uploadDataDuringCreation", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationParallelUploadsWithUploadDataDuringCreation" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: cannot use the `parallelUploadBoundaries` option when `parallelUploads` is disabled", + "completionReason": "parallelBoundariesWithoutParallelUploads", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "parallelUploadBoundaries", + "value": [ + { + "end": 5, + "start": 0 + } + ] + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationParallelBoundariesWithoutParallelUploads" + }, + { + "behavior": "start-option-validation", + "completionKind": "error", + "completionMessage": "tus: the `parallelUploadBoundaries` must have the same length as the value of `parallelUploads`", + "completionReason": "parallelBoundariesLengthMismatch", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "startOptionValidation", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "parallelUploads", + "value": 2 + }, + { + "key": "parallelUploadBoundaries", + "value": [ + { + "end": 5, + "start": 0 + } + ] + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [], + "primitives": [ + "validate-start-options" + ], + "requests": [], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "startValidationParallelBoundariesLengthMismatch" + }, + { + "behavior": "detailed-error", + "completionKind": "error", + "completionMessage": "tus: unexpected response while creating upload, originated from request (method: POST, url: https://tus.io/uploads, response code: 500, response text: server_error, request id: contract-request-id)", + "completionReason": "unexpectedCreateResponse", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "detailedErrors", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "headers", + "value": { + "X-Request-ID": "contract-request-id" + } + }, + { + "key": "rawOptions", + "value": { + "retryDelays": null + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload" + ], + "primitives": [ + "report-detailed-errors" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Request-ID": "contract-request-id" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": "server_error", + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 500, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Request-ID": "contract-request-id" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "detailedCreateResponseError" + }, + { + "behavior": "detailed-error", + "completionKind": "error", + "completionMessage": "tus: failed to create upload, caused by Error: socket down, originated from request (method: POST, url: https://tus.io/uploads, response code: n/a, response text: n/a, request id: contract-request-id)", + "completionReason": "createUploadRequestFailed", + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "detailedErrors", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "headers", + "value": { + "X-Request-ID": "contract-request-id" + } + }, + { + "key": "rawOptions", + "value": { + "retryDelays": null + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload" + ], + "primitives": [ + "report-detailed-errors" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": "socket down", + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Request-ID": "contract-request-id" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": null, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Request-ID": "contract-request-id" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "detailedCreateRequestError" + }, + { + "behavior": "upload-body-headers", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/upload-body-headers-contract", + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "uploadBodyHeaders", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "send-upload-body-headers" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/upload-body-headers-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/upload-body-headers-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/upload-body-headers-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "uploadBodyHeaders" + }, + { + "behavior": "custom-request-headers", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/custom-headers-contract", + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "customRequestHeaders", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "headers", + "value": { + "X-Tus-Contract": "custom-header", + "X-Tus-Trace": "trace-123" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "apply-custom-request-headers" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Tus-Contract": "custom-header", + "X-Tus-Trace": "trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/custom-headers-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/custom-headers-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Tus-Contract": "custom-header", + "X-Tus-Trace": "trace-123" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "custom-header", + "X-Tus-Trace": "trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "custom-header", + "X-Tus-Trace": "trace-123" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/custom-headers-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": "contract-custom-headers-fingerprint" + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "customRequestHeaders" + }, + { + "behavior": "request-id-headers", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/request-id-contract", + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "requestIdHeaders", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "headers", + "value": { + "X-Request-ID": "custom-request-id" + } + }, + { + "key": "addRequestId", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "add-request-id-header", + "apply-custom-request-headers" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Request-ID": "00000000-0000-4000-8000-000000000000" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/request-id-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/request-id-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Request-ID": "00000000-0000-4000-8000-000000000000" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Request-ID": "00000000-0000-4000-8000-000000000000" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Request-ID": "00000000-0000-4000-8000-000000000000" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/request-id-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": true, + "generatedRequestId": "00000000-0000-4000-8000-000000000000" + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "requestIdHeaders" + }, + { + "behavior": "resume-from-previous-upload", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/resume-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "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" + ], + "eventKinds": [ + "fingerprint", + "url-storage-find", + "upload-url-available", + "progress", + "chunk-complete", + "url-storage-remove", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [ + { + "actions": [ + { + "expectedPreviousUploadCount": 1, + "kind": "resume-from-previous-upload", + "selectedPreviousUploadIndex": 0 + } + ], + "phase": "beforeStart" + } + ], + "featureId": "resumeUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "removeFingerprintOnSuccess", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Length": "11", + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "recover-upload-offset", + "uploadUrl": null, + "url": "upload", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/resume-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 6, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/resume-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": "contract-resume-fingerprint" + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": true, + "storedUpload": { + "fingerprint": "contract-resume-fingerprint", + "uploadUrl": "https://tus.io/uploads/resume-contract", + "urlStorageKey": "tus::contract-resume-fingerprint::1337" + } + } + }, + "scenarioId": "resumeFromPreviousUpload" + }, + { + "behavior": "relative-location-resolution", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/files/relative-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close" + ], + "eventKinds": [ + "upload-url-available", + "progress", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "relativeLocationResolution", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/files/" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "resolve-relative-location" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "relative-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "relative-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/files/" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/files/relative-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "relativeLocationResolution" + }, + { + "behavior": "array-buffer-input", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/array-buffer-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "source-open:array-buffer:11", + "success", + "source-close" + ], + "eventKinds": [ + "source-open", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "inputSources", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "array-buffer" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "read-browser-file" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/array-buffer-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/array-buffer-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/array-buffer-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "arrayBufferInput" + }, + { + "behavior": "array-buffer-view-input", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/array-buffer-view-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "source-open:array-buffer-view:11", + "success", + "source-close" + ], + "eventKinds": [ + "source-open", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "inputSources", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "array-buffer-view" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "read-browser-file" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/array-buffer-view-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/array-buffer-view-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/array-buffer-view-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "arrayBufferViewInput" + }, + { + "behavior": "web-readable-stream-input", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/web-stream-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "source-open:web-readable-stream:null", + "success", + "source-close" + ], + "eventKinds": [ + "source-open", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "inputSources", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 100 + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "uploadLengthDeferred", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "web-readable-stream" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "read-web-stream" + ], + "requests": [ + { + "absentHeaders": [ + "Upload-Length" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/web-stream-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/web-stream-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/web-stream-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "webReadableStreamInput" + }, + { + "behavior": "node-readable-stream-input", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/node-stream-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "source-open:node-readable-stream:null", + "success", + "source-close" + ], + "eventKinds": [ + "source-open", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "inputSources", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 100 + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "uploadLengthDeferred", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "node-readable-stream" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "read-node-stream" + ], + "requests": [ + { + "absentHeaders": [ + "Upload-Length" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/node-stream-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/node-stream-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": null, + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/node-stream-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "nodeReadableStreamInput", + "runtimes": [ + "node" + ] + }, + { + "behavior": "node-path-input", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/node-path-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "source-open:node-path-reference:11", + "success", + "source-close" + ], + "eventKinds": [ + "source-open", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "inputSources", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "node-path-reference" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "read-node-file" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/node-path-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/node-path-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/node-path-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "nodePathInput", + "runtimes": [ + "node" + ] + }, + { + "behavior": "deferred-length-upload", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/deferred-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close" + ], + "eventKinds": [ + "upload-url-available", + "progress", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "deferredLengthBytesTotal": "allow-known-total-before-declaration", + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "deferredLengthUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 100 + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "uploadLengthDeferred", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "web-readable-stream" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "defer-upload-length", + "emit-progress" + ], + "requests": [ + { + "absentHeaders": [ + "Upload-Length" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/deferred-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/deferred-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/deferred-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "deferredLengthUpload" + }, + { + "behavior": "deferred-length-upload", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/deferred-chunked-contract", + "eventKeyAlternativeGroups": [ + [], + [ + "progress:0:11" + ], + [ + "progress:5:11" + ], + [ + "chunk-complete:5:5:11" + ], + [ + "progress:5:11" + ], + [ + "progress:10:11" + ], + [ + "chunk-complete:5:10:11" + ], + [], + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "upload-url-available", + "progress:0:null", + "progress:5:null", + "chunk-complete:5:5:null", + "progress:5:null", + "progress:10:null", + "chunk-complete:5:10:null", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close" + ], + "eventKinds": [ + "upload-url-available", + "progress", + "chunk-complete", + "success", + "source-close" + ], + "eventPolicy": { + "deferredLengthBytesTotal": "allow-known-total-before-declaration", + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [], + "featureId": "deferredLengthUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 5 + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "uploadLengthDeferred", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "defer-upload-length", + "emit-chunk-complete", + "emit-progress" + ], + "requests": [ + { + "absentHeaders": [ + "Upload-Length" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/deferred-chunked-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/deferred-chunked-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Defer-Length": "1", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": 0, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/deferred-chunked-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "10" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "10", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/deferred-chunked-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 1, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "10" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-final-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 3, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "10" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/deferred-chunked-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "deferredLengthChunkedUpload" + }, + { + "behavior": "override-patch-method", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/override-contract", + "eventKeyAlternativeGroups": [], + "eventKeyExtraPrefixes": [], + "eventKeys": [], + "eventKinds": [], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "overridePatchMethod", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "overridePatchMethod", + "value": true + }, + { + "key": "uploadUrl", + "value": "https://tus.io/uploads/override-contract" + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "override-patch-method" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Offset": "3" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Length": "11", + "Upload-Offset": "3", + "Tus-Resumable": "1.0.0" + } + }, + "role": "recover-upload-offset", + "uploadUrl": "https://tus.io/uploads/override-contract", + "url": "upload", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/override-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 8, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "3" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": "https://tus.io/uploads/override-contract", + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "3", + "X-HTTP-Method-Override": "PATCH" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads/override-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": true, + "value": "contract-override-fingerprint" + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "overridePatchMethod" + }, + { + "behavior": "parallel-upload-concat", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/parallel-final", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [ + "progress:" + ], + "eventKeys": [ + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11" + ], + "eventKinds": [ + "progress", + "chunk-complete" + ], + "eventPolicy": { + "matching": "exact-except-allowed-extra-events", + "progress": "milestone", + "transportProgress": "may-emit-extra-samples" + }, + "executionActionPhases": [ + { + "actions": [ + { + "gateId": "parallel-patches", + "heldRequestIndexes": [ + 2, + 3 + ], + "kind": "release-after-all-started", + "releaseAfterRequestIndexes": [ + 2, + 3 + ], + "timeoutMs": 2000 + } + ], + "phase": "serverRequestGates" + } + ], + "featureId": "parallelUploadConcat", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "foo": "hello" + } + }, + { + "key": "metadataForPartialUploads", + "value": { + "test": "world" + } + }, + { + "key": "parallelUploads", + "value": 2 + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "createTusUpload" + ], + "primitives": [ + "concatenate-partial-uploads", + "emit-progress" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/parallel-part-1" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/parallel-part-1", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-partial-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "5" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "6" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/parallel-part-2" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/parallel-part-2", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-partial-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "6" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-partial-chunk", + "uploadUrl": "https://tus.io/uploads/parallel-part-1", + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/parallel-part-1" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 6, + "bodyStart": 5, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "6" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "6", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-partial-chunk", + "uploadUrl": "https://tus.io/uploads/parallel-part-2", + "url": "upload", + "requestIndex": 3, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/parallel-part-2" + }, + { + "absentHeaders": [ + "Upload-Length" + ], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Metadata": "foo aGVsbG8=", + "Upload-Concat": "final;https://tus.io/uploads/parallel-part-1 https://tus.io/uploads/parallel-part-2" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/parallel-final" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/parallel-final", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-final-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 4, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Metadata": "foo aGVsbG8=", + "Upload-Concat": "final;https://tus.io/uploads/parallel-part-1 https://tus.io/uploads/parallel-part-2" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "parallelUploadConcat" + }, + { + "behavior": "parallel-upload-abort-cleanup", + "completionKind": "aborted", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [ + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "request-abort:3" + ], + "eventKinds": [ + "request-abort" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [ + { + "actions": [ + { + "gateId": "parallel-cleanup-patches", + "heldRequestIndexes": [ + 2, + 3 + ], + "kind": "release-after-all-started", + "releaseAfterRequestIndexes": [ + 2, + 3 + ], + "timeoutMs": 2000 + } + ], + "phase": "serverRequestGates" + } + ], + "featureId": "parallelUploadConcat", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadataForPartialUploads", + "value": { + "test": "world" + } + }, + { + "key": "headers", + "value": { + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + } + }, + { + "key": "overridePatchMethod", + "value": true + }, + { + "key": "parallelUploads", + "value": 2 + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload" + ], + "primitives": [ + "abort-current-request", + "terminate-upload", + "concatenate-partial-uploads" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "5", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/parallel-cleanup-part-1" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/parallel-cleanup-part-1", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-partial-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "5", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "6", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/parallel-cleanup-part-2" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/parallel-cleanup-part-2", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-partial-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Metadata": "test d29ybGQ=", + "Upload-Concat": "partial", + "Upload-Length": "6", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": 0, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 500, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-partial-chunk", + "uploadUrl": "https://tus.io/uploads/parallel-cleanup-part-1", + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123", + "X-HTTP-Method-Override": "PATCH" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads/parallel-cleanup-part-1" + }, + { + "absentHeaders": [], + "abort": true, + "bodySize": 6, + "bodyStart": 5, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": null, + "role": "upload-partial-chunk", + "uploadUrl": "https://tus.io/uploads/parallel-cleanup-part-2", + "url": "upload", + "requestIndex": 3, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123", + "X-HTTP-Method-Override": "PATCH" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads/parallel-cleanup-part-2" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "terminateTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 204, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "terminate-upload", + "uploadUrl": "https://tus.io/uploads/parallel-cleanup-part-1", + "url": "upload", + "requestIndex": 4, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "effectiveMethod": "DELETE", + "expectedUrl": "https://tus.io/uploads/parallel-cleanup-part-1" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "terminateTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 204, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "terminate-upload", + "uploadUrl": "https://tus.io/uploads/parallel-cleanup-part-2", + "url": "upload", + "requestIndex": 5, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "X-Tus-Contract": "parallel-cleanup-policy", + "X-Tus-Trace": "parallel-cleanup-trace-123" + }, + "effectiveMethod": "DELETE", + "expectedUrl": "https://tus.io/uploads/parallel-cleanup-part-2" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": true + }, + "fingerprint": { + "install": true, + "value": "contract-parallel-cleanup-fingerprint" + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "parallelUploadAbortCleanup" + }, + { + "behavior": "retry-patch-after-offset-recovery", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/retry-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0" + ], + "eventKinds": [ + "should-retry", + "retry-schedule" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "retryOffsetRecovery", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "retryDelays", + "value": [ + 0 + ] + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "retry-with-backoff", + "recover-offset-after-error" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/retry-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/retry-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 500, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/retry-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Length": "11", + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "recover-upload-offset", + "uploadUrl": null, + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/retry-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 6, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 500, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "retry-upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 3, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/retry-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Length": "11", + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "recover-upload-offset", + "uploadUrl": null, + "url": "upload", + "requestIndex": 4, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/retry-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 6, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-final-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 5, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "5" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/retry-contract" + } + ], + "retryDecisions": [ + { + "decision": true, + "retryAttempt": 0 + }, + { + "decision": true, + "retryAttempt": 0 + } + ], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "retryPatchAfterOffsetRecovery" + }, + { + "behavior": "request-lifecycle-hooks", + "completionKind": "success", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/request-hooks-contract", + "eventKeyAlternativeGroups": [ + [], + [], + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "before-request:0", + "after-response:0", + "success", + "source-close" + ], + "eventKinds": [ + "before-request", + "after-response", + "success", + "source-close" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [], + "featureId": "requestLifecycleHooks", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "uploadUrl", + "value": "https://tus.io/uploads/request-hooks-contract" + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "getTusUploadOffset" + ], + "primitives": [ + "run-request-hooks" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "getTusUploadOffset", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Offset": "11" + }, + "headersSpecified": true, + "statusCode": 200, + "effectiveHeaders": { + "Upload-Length": "11", + "Upload-Offset": "11", + "Tus-Resumable": "1.0.0" + } + }, + "role": "recover-upload-offset", + "uploadUrl": null, + "url": "upload", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "HEAD", + "expectedUrl": "https://tus.io/uploads/request-hooks-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "requestLifecycleHooks" + }, + { + "behavior": "abort-upload", + "completionKind": "aborted", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": null, + "eventKeyAlternativeGroups": [ + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "request-abort:0" + ], + "eventKinds": [ + "request-abort" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [ + { + "actions": [ + { + "kind": "cancel-upload", + "requestIndex": 0 + } + ], + "phase": "onRequestStart" + } + ], + "featureId": "abortUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload" + ], + "primitives": [ + "abort-current-request" + ], + "requests": [ + { + "absentHeaders": [], + "abort": true, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": null, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "abortUpload" + }, + { + "behavior": "abort-upload-after-stored-url", + "completionKind": "aborted", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/abort-terminate-contract", + "eventKeyAlternativeGroups": [ + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "request-abort:1" + ], + "eventKinds": [ + "request-abort" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [ + { + "actions": [ + { + "kind": "cancel-upload", + "requestIndex": 1 + } + ], + "phase": "onRequestStart" + } + ], + "featureId": "abortUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "headers", + "value": { + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123" + } + }, + { + "key": "overridePatchMethod", + "value": true + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload", + "terminateTusUpload" + ], + "primitives": [ + "abort-current-request", + "terminate-upload" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/abort-terminate-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/abort-terminate-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0", + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": true, + "bodySize": 11, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": null, + "role": "abort-upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0", + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123", + "X-HTTP-Method-Override": "PATCH" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads/abort-terminate-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123" + }, + "headersSpecified": true, + "method": null, + "operationId": "terminateTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 204, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "terminate-upload", + "uploadUrl": null, + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "X-Tus-Contract": "abort-policy", + "X-Tus-Trace": "abort-trace-123" + }, + "effectiveMethod": "DELETE", + "expectedUrl": "https://tus.io/uploads/abort-terminate-contract" + } + ], + "retryDecisions": [], + "runtimeSetup": { + "abort": { + "terminateUpload": true + }, + "fingerprint": { + "install": true, + "value": "contract-abort-terminate-fingerprint" + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "abortUploadAfterStoredUrl" + }, + { + "behavior": "terminate-with-retry", + "completionKind": "terminated", + "completionMessage": null, + "completionReason": null, + "completionUploadUrl": "https://tus.io/uploads/terminate-contract", + "eventKeyAlternativeGroups": [ + [], + [] + ], + "eventKeyExtraPrefixes": [], + "eventKeys": [ + "should-retry:0:true", + "retry-schedule:0" + ], + "eventKinds": [ + "should-retry", + "retry-schedule" + ], + "eventPolicy": { + "matching": "exact" + }, + "executionActionPhases": [ + { + "actions": [ + { + "kind": "abort-upload", + "terminateUpload": true + } + ], + "phase": "onChunkComplete" + } + ], + "featureId": "terminateUpload", + "inputOptionEntries": [ + { + "key": "endpointUrl", + "value": "https://tus.io/uploads" + }, + { + "key": "chunkSize", + "value": 5 + }, + { + "key": "metadata", + "value": { + "filename": "hello.txt" + } + }, + { + "key": "retryDelays", + "value": [ + 0, + 0 + ] + } + ], + "inputSource": { + "content": "hello world", + "kind": "blob" + }, + "operationIds": [ + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload" + ], + "primitives": [ + "terminate-upload", + "retry-with-backoff" + ], + "requests": [ + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "headersSpecified": true, + "method": null, + "operationId": "createTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Location": "https://tus.io/uploads/terminate-contract" + }, + "headersSpecified": true, + "statusCode": 201, + "effectiveHeaders": { + "Location": "https://tus.io/uploads/terminate-contract", + "Tus-Resumable": "1.0.0" + } + }, + "role": "create-upload", + "uploadUrl": null, + "url": "endpoint", + "requestIndex": 0, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Upload-Length": "11", + "Upload-Metadata": "filename aGVsbG8udHh0" + }, + "effectiveMethod": "POST", + "expectedUrl": "https://tus.io/uploads" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": 5, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": { + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "headersSpecified": true, + "method": null, + "operationId": "patchTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": { + "Upload-Offset": "5" + }, + "headersSpecified": true, + "statusCode": 204, + "effectiveHeaders": { + "Upload-Offset": "5", + "Tus-Resumable": "1.0.0" + } + }, + "role": "upload-chunk", + "uploadUrl": null, + "url": "upload", + "requestIndex": 1, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0", + "Content-Type": "application/offset+octet-stream", + "Upload-Offset": "0" + }, + "effectiveMethod": "PATCH", + "expectedUrl": "https://tus.io/uploads/terminate-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "terminateTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 423, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "terminate-upload", + "uploadUrl": null, + "url": "upload", + "requestIndex": 2, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "DELETE", + "expectedUrl": "https://tus.io/uploads/terminate-contract" + }, + { + "absentHeaders": [], + "abort": false, + "bodySize": null, + "bodyStart": null, + "errorMessage": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "method": null, + "operationId": "terminateTusUpload", + "response": { + "body": null, + "headerMode": null, + "headers": {}, + "headersSpecified": false, + "statusCode": 204, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + } + }, + "role": "retry-terminate-upload", + "uploadUrl": null, + "url": "upload", + "requestIndex": 3, + "effectiveHeaders": { + "Tus-Resumable": "1.0.0" + }, + "effectiveMethod": "DELETE", + "expectedUrl": "https://tus.io/uploads/terminate-contract" + } + ], + "retryDecisions": [ + { + "decision": true, + "retryAttempt": 0 + } + ], + "runtimeSetup": { + "abort": { + "terminateUpload": false + }, + "fingerprint": { + "install": false, + "value": null + }, + "requestId": { + "enabled": false, + "generatedRequestId": null + }, + "urlStorage": { + "install": false, + "storedUpload": null + } + }, + "scenarioId": "terminateWithRetry" + } + ], + "clientFeatures": [ + { + "conformance": { + "scenarioIds": [ + "singleUploadLifecycle" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Create an upload, store its URL, upload bytes, and finish successfully.", + "featureId": "singleUploadLifecycle", + "flow": [ + { + "kind": "primitive", + "primitive": "open-input-source", + "summary": "Open the caller input as a sliceable source." + }, + { + "kind": "operation", + "operationId": "createTusUpload", + "summary": "Create the remote upload resource." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Upload bytes until the accepted offset reaches the known length." + } + ], + "operationIds": [ + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request" + ], + "sdkPlan": { + "kind": "http-sequence", + "requests": [ + { + "body": { + "kind": "empty" + }, + "headers": [ + { + "kind": "compatibility-version" + }, + { + "contentInput": "content", + "kind": "upload-length" + }, + { + "kind": "metadata", + "metadataInput": "metadata" + }, + { + "kind": "upload-complete", + "state": "incomplete" + } + ], + "kind": "request", + "operationRole": "creation", + "requestId": "create", + "response": { + "captures": [ + { + "captureId": "uploadUrl", + "headerRef": "location", + "kind": "header-url", + "resolveRelativeToInput": "endpointUrl" + } + ] + }, + "url": { + "input": "endpointUrl", + "kind": "input-url" + } + }, + { + "body": { + "contentInput": "content", + "kind": "bytes" + }, + "headers": [ + { + "kind": "compatibility-version" + }, + { + "kind": "upload-offset", + "value": "0" + }, + { + "kind": "upload-body-content-type" + }, + { + "kind": "upload-complete", + "state": "complete" + } + ], + "kind": "request", + "operationRole": "upload-chunk", + "requestId": "upload", + "response": { + "assertions": [ + { + "contentInput": "content", + "headerRef": "uploadOffset", + "kind": "header-int-equals-content-length" + } + ] + }, + "url": { + "captureId": "uploadUrl", + "kind": "captured-url" + } + } + ] + } + }, + { + "conformance": { + "scenarioIds": [ + "resumeFromPreviousUpload" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Resume a stored upload URL by discovering the remote offset before patching.", + "featureId": "resumeUpload", + "flow": [ + { + "kind": "primitive", + "primitive": "resume-from-previous-upload", + "summary": "Load a stored upload URL selected by fingerprint." + }, + { + "kind": "operation", + "operationId": "getTusUploadOffset", + "summary": "Read the server offset for the stored upload URL." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Continue uploading from the discovered offset." + } + ], + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url" + ], + "sdkPlan": { + "kind": "http-sequence", + "requests": [ + { + "body": { + "kind": "empty" + }, + "headers": [ + { + "kind": "compatibility-version" + } + ], + "kind": "request", + "operationRole": "offset-discovery", + "requestId": "offset", + "response": { + "captures": [ + { + "captureId": "resumeOffset", + "headerRef": "uploadOffset", + "kind": "header-int" + } + ] + }, + "url": { + "input": "storedUploadUrl", + "kind": "input-url" + } + }, + { + "body": { + "contentInput": "content", + "kind": "bytes-from-offset", + "offsetCaptureId": "resumeOffset" + }, + "headers": [ + { + "kind": "compatibility-version" + }, + { + "captureId": "resumeOffset", + "kind": "upload-offset-from-capture" + }, + { + "kind": "upload-body-content-type" + }, + { + "kind": "upload-complete", + "state": "complete" + } + ], + "kind": "request", + "operationRole": "upload-chunk", + "requestId": "upload", + "response": { + "assertions": [ + { + "contentInput": "content", + "headerRef": "uploadOffset", + "kind": "header-int-equals-content-length" + } + ] + }, + "url": { + "input": "storedUploadUrl", + "kind": "input-url" + } + } + ] + } + }, + { + "conformance": { + "scenarioIds": [ + "deferredLengthUpload", + "deferredLengthChunkedUpload" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Create an upload without a known length and declare the length on the final upload request.", + "featureId": "deferredLengthUpload", + "flow": [ + { + "kind": "operation", + "operationId": "createTusUpload", + "summary": "Create the upload with deferred length." + }, + { + "kind": "primitive", + "primitive": "defer-upload-length", + "summary": "Track the source until the final upload request reveals the total size." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Declare Upload-Length on the final upload request." + } + ], + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "defer-upload-length", + "emit-chunk-complete", + "emit-progress" + ] + }, + { + "conformance": { + "scenarioIds": [ + "creationWithUpload", + "creationWithUploadPartialChunk" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Send the first bytes on the creation request when the server/client support it.", + "featureId": "creationWithUpload", + "flow": [ + { + "kind": "operation", + "operationId": "createTusUpload", + "summary": "Create the upload while streaming the initial body." + }, + { + "kind": "primitive", + "primitive": "upload-during-creation", + "summary": "Interpret the creation response as an accepted offset." + } + ], + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "upload-during-creation", + "emit-progress" + ] + }, + { + "conformance": { + "scenarioIds": [ + "uploadBodyHeaders" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Send protocol-specific upload body headers whenever the client transmits file bytes.", + "featureId": "uploadBodyHeaders", + "flow": [ + { + "kind": "primitive", + "primitive": "send-upload-body-headers", + "summary": "Attach the protocol-specific upload body content type when a request has bytes." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Upload bytes with the protocol-specific body headers." + } + ], + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "send-upload-body-headers" + ] + }, + { + "conformance": { + "scenarioIds": [ + "customRequestHeaders" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Apply user-provided request headers to every upload request.", + "featureId": "customRequestHeaders", + "flow": [ + { + "kind": "primitive", + "primitive": "apply-custom-request-headers", + "summary": "Merge user-provided headers after protocol headers are prepared." + }, + { + "kind": "operation", + "operationId": "createTusUpload", + "summary": "Create uploads with the configured custom headers." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Upload bytes with the configured custom headers." + } + ], + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "apply-custom-request-headers" + ] + }, + { + "conformance": { + "scenarioIds": [ + "requestIdHeaders" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Add generated request IDs after protocol and custom request headers.", + "featureId": "requestIdHeaders", + "flow": [ + { + "kind": "primitive", + "primitive": "add-request-id-header", + "summary": "Generate a request ID and apply it after custom request headers so it is authoritative." + }, + { + "kind": "operation", + "operationId": "createTusUpload", + "summary": "Create uploads with a generated request ID." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Upload bytes with a generated request ID." + } + ], + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "add-request-id-header", + "apply-custom-request-headers" + ] + }, + { + "conformance": { + "scenarioIds": [ + "overridePatchMethod" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Tunnel PATCH through POST with the method-override header.", + "featureId": "overridePatchMethod", + "flow": [ + { + "kind": "operation", + "operationId": "getTusUploadOffset", + "summary": "Resume from the upload URL before sending bytes." + }, + { + "kind": "primitive", + "primitive": "override-patch-method", + "summary": "Replace PATCH with POST while preserving the protocol operation intent." + }, + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Upload bytes through the overridden request." + } + ], + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "override-patch-method" + ] + }, + { + "conformance": { + "scenarioIds": [ + "parallelUploadConcat", + "parallelUploadAbortCleanup" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Split one input into partial uploads, run the parts concurrently, clean up aborted parts, and concatenate their upload URLs.", + "featureId": "parallelUploadConcat", + "flow": [ + { + "kind": "primitive", + "primitive": "split-parallel-upload-boundaries", + "summary": "Split the input into stable byte ranges." + }, + { + "kind": "operation", + "operationId": "createTusUpload", + "summary": "Create partial uploads for each range." + }, + { + "kind": "primitive", + "primitive": "concatenate-partial-uploads", + "summary": "Create the final upload from completed partial upload URLs." + } + ], + "operationIds": [ + "createTusUpload", + "patchTusUpload" + ], + "primitives": [ + "abort-current-request", + "concatenate-partial-uploads", + "emit-progress", + "split-parallel-upload-boundaries", + "terminate-upload" + ] + }, + { + "conformance": { + "scenarioIds": [ + "retryPatchAfterOffsetRecovery" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Recover from a failed chunk by reading the server offset before retrying.", + "featureId": "retryOffsetRecovery", + "flow": [ + { + "kind": "operation", + "operationId": "patchTusUpload", + "summary": "Attempt the chunk upload." + }, + { + "kind": "primitive", + "primitive": "recover-offset-after-error", + "summary": "Discover the accepted offset after a retryable failure." + }, + { + "kind": "operation", + "operationId": "getTusUploadOffset", + "summary": "Use HEAD to recover the offset before retrying PATCH." + } + ], + "operationIds": [ + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "retry-with-backoff", + "recover-offset-after-error" + ] + }, + { + "conformance": { + "scenarioIds": [ + "retryPatchAfterOffsetRecovery" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Schedule retry timers and reset retry attempts after accepted progress.", + "featureId": "retryStateTransitions", + "flow": [ + { + "kind": "primitive", + "primitive": "schedule-retry-timer", + "summary": "Consume the current retry delay and restart the upload after that timer fires." + }, + { + "kind": "primitive", + "primitive": "reset-retry-attempt-after-progress", + "summary": "Reset retry attempts once a later retry observes server-side offset progress." + } + ], + "operationIds": [ + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "retry-with-backoff", + "schedule-retry-timer", + "reset-retry-attempt-after-progress" + ] + }, + { + "conformance": { + "scenarioIds": [ + "terminateWithRetry" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Terminate an upload resource and retry retryable termination failures.", + "featureId": "terminateUpload", + "flow": [ + { + "kind": "primitive", + "primitive": "terminate-upload", + "summary": "Choose server-side termination for an upload URL." + }, + { + "kind": "operation", + "operationId": "terminateTusUpload", + "summary": "Delete the upload resource." + } + ], + "operationIds": [ + "terminateTusUpload" + ], + "primitives": [ + "terminate-upload", + "retry-with-backoff" + ] + }, + { + "conformance": { + "scenarioIds": [ + "abortUpload", + "abortUploadAfterStoredUrl" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Abort the active request, pending retry timer, and any partial uploads.", + "featureId": "abortUpload", + "flow": [ + { + "kind": "primitive", + "primitive": "abort-current-request", + "summary": "Cancel in-flight transport work without emitting user callbacks after abort." + } + ], + "operationIds": [ + "terminateTusUpload" + ], + "primitives": [ + "abort-current-request", + "terminate-upload" + ] + }, + { + "conformance": { + "scenarioIds": [ + "singleUploadLifecycle", + "creationWithUpload", + "resumeFromPreviousUpload" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Expose progress and accepted-chunk callbacks from runtime upload activity.", + "featureId": "uploadCallbacks", + "flow": [ + { + "kind": "primitive", + "primitive": "emit-progress", + "summary": "Report bytes sent against known or deferred length." + }, + { + "kind": "primitive", + "primitive": "emit-chunk-complete", + "summary": "Report chunk size, accepted offset, and total size after server acceptance." + }, + { + "kind": "primitive", + "primitive": "emit-upload-url", + "summary": "Notify once a usable upload URL is known." + } + ], + "operationIds": [], + "primitives": [ + "emit-progress", + "emit-chunk-complete", + "emit-upload-url" + ] + }, + { + "conformance": { + "scenarioIds": [ + "requestLifecycleHooks", + "retryPatchAfterOffsetRecovery" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Run before-request, after-response, and custom retry hooks around transport.", + "featureId": "requestLifecycleHooks", + "flow": [ + { + "kind": "primitive", + "primitive": "run-request-hooks", + "summary": "Call user hooks around each HTTP request/response pair." + }, + { + "kind": "primitive", + "primitive": "customize-retry", + "summary": "Let user retry policy override default retry decisions." + } + ], + "operationIds": [], + "primitives": [ + "customize-retry", + "run-request-hooks" + ] + }, + { + "conformance": { + "scenarioIds": [ + "singleUploadLifecycle", + "resumeFromPreviousUpload" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Persist, find, resume, and optionally remove upload URLs by fingerprint.", + "featureId": "resumeUrlStorage", + "flow": [ + { + "kind": "primitive", + "primitive": "fingerprint-input", + "summary": "Derive a stable key for the input when possible." + }, + { + "kind": "primitive", + "primitive": "store-resume-url", + "summary": "Persist upload URLs and partial-upload URLs for future resumption." + }, + { + "kind": "primitive", + "primitive": "remove-stored-url-on-success", + "summary": "Remove stored upload URLs when configured after success or invalidation." + } + ], + "operationIds": [], + "primitives": [ + "fingerprint-input", + "store-resume-url", + "remove-stored-url-on-success" + ] + }, + { + "conformance": { + "scenarioIds": [ + "arrayBufferInput", + "arrayBufferViewInput", + "webReadableStreamInput", + "nodeReadableStreamInput", + "nodePathInput" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Support the reference client input/source families across runtimes.", + "featureId": "inputSources", + "flow": [ + { + "kind": "primitive", + "primitive": "read-browser-file", + "summary": "Read browser Blob/File and ArrayBuffer-family inputs." + }, + { + "kind": "primitive", + "primitive": "read-node-stream", + "summary": "Read Node streams when size and chunk constraints are satisfied." + }, + { + "kind": "primitive", + "primitive": "read-web-stream", + "summary": "Read Web Streams with deferred or configured size." + }, + { + "kind": "primitive", + "primitive": "read-node-file", + "summary": "Read filesystem paths and fs streams, including parallel ranges." + } + ], + "operationIds": [], + "primitives": [ + "read-browser-file", + "read-node-file", + "read-node-stream", + "read-web-stream" + ] + }, + { + "conformance": { + "scenarioIds": [ + "webStorageUrlStorageBackend", + "fileUrlStorageBackend" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Support browser and file-backed URL storage implementations.", + "featureId": "urlStorageBackends", + "flow": [ + { + "kind": "primitive", + "primitive": "store-browser-url", + "summary": "Persist upload records in browser localStorage." + }, + { + "kind": "primitive", + "primitive": "store-file-url", + "summary": "Persist upload records in the Node file store." + } + ], + "operationIds": [], + "primitives": [ + "store-browser-url", + "store-file-url" + ] + }, + { + "conformance": { + "scenarioIds": [ + "ietfDraft05CreationWithUpload", + "ietfDraft05ChunkedUploadComplete", + "ietfDraft03ResumeWithoutKnownLength" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Select between tus v1 and supported IETF draft client protocol modes.", + "featureId": "protocolVersionSelection", + "flow": [ + { + "kind": "primitive", + "primitive": "select-client-protocol", + "summary": "Choose request headers and response expectations for the selected protocol." + } + ], + "operationIds": [ + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload" + ], + "primitives": [ + "select-client-protocol" + ] + }, + { + "conformance": { + "scenarioIds": [ + "relativeLocationResolution" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Normalize relative Location headers against the request endpoint.", + "featureId": "relativeLocationResolution", + "flow": [ + { + "kind": "primitive", + "primitive": "resolve-relative-location", + "summary": "Resolve server Location headers with the creation endpoint as origin." + } + ], + "operationIds": [ + "createTusUpload" + ], + "primitives": [ + "resolve-relative-location" + ] + }, + { + "conformance": { + "scenarioIds": [ + "startValidationMissingInput", + "startValidationMissingEndpointOrUploadUrl", + "startValidationUnsupportedProtocol", + "startValidationRetryDelaysNotArray", + "startValidationParallelUploadsWithUploadUrl", + "startValidationParallelUploadsWithUploadSize", + "startValidationParallelUploadsWithDeferredLength", + "startValidationParallelUploadsWithUploadDataDuringCreation", + "startValidationParallelBoundariesWithoutParallelUploads", + "startValidationParallelBoundariesLengthMismatch" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Validate option combinations before starting runtime work.", + "featureId": "startOptionValidation", + "flow": [ + { + "kind": "primitive", + "primitive": "validate-start-options", + "summary": "Reject missing inputs and incompatible parallel/deferred/resume options." + } + ], + "operationIds": [], + "primitives": [ + "validate-start-options" + ] + }, + { + "conformance": { + "scenarioIds": [ + "detailedCreateResponseError", + "detailedCreateRequestError" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Attach request, response, status, body, and request ID context to errors.", + "featureId": "detailedErrors", + "flow": [ + { + "kind": "primitive", + "primitive": "report-detailed-errors", + "summary": "Return user-facing errors with enough transport context for debugging." + } + ], + "operationIds": [], + "primitives": [ + "report-detailed-errors" + ] + } + ], + "clientFlow": { + "abort": { + "error": { + "message": "Request was aborted", + "name": "AbortError", + "type": "DOMException" + }, + "removeStoredUrlAfterTermination": "after-successful-termination", + "sequence": [ + "mark-aborted", + "abort-parallel-uploads", + "abort-current-request", + "clear-retry-timer", + "terminate-upload-if-requested" + ], + "suppressErrorAfterAbort": true, + "terminateUpload": "when-requested-and-upload-url-known", + "terminateUploadContext": "detached-from-aborted-request" + }, + "creationWithUpload": { + "bodySource": "first-upload-chunk", + "completion": "continue-with-patch-when-offset-less-than-size", + "extensionName": "creation-with-upload", + "responseOffset": "accepted-offset" + }, + "deferredLength": { + "createSize": "size-unknown", + "declareLength": "final-upload-request", + "extensionName": "creation-defer-length" + }, + "detailedErrors": { + "causeStringTemplate": "Error: {message}", + "causedByTemplate": ", caused by {cause}", + "emptyResponseBody": "", + "missingValue": "n/a", + "requestContextTemplate": ", originated from request (method: {method}, url: {url}, response code: {status}, response text: {body}, request id: {requestId})" + }, + "eventHooks": { + "chunkComplete": { + "afterChunkAccepted": "accepted-chunk-size-and-offset" + }, + "progress": { + "afterChunkAccepted": "accepted-offset", + "afterResumeAlreadyComplete": "upload-length", + "beforeRequestBody": "current-offset", + "duringRequest": "start-offset-plus-transmitted-bytes", + "parallelPartProgress": "aggregated-part-progress" + }, + "success": { + "closeSource": "after-hook-when-source-open", + "emit": "after-upload-complete", + "removeStoredUrl": "before-hook-when-option-enabled" + }, + "uploadUrlAvailable": { + "contexts": { + "createUpload": "createUpload", + "parallelFinalUpload": "parallelFinalUpload", + "resumeUpload": "resumeUpload" + }, + "createUpload": "after-url-known-before-storage", + "parallelFinalUpload": "not-emitted", + "resumeUpload": "after-url-known-before-storage" + } + }, + "fileSources": { + "commonTypes": [ + "File", + "Blob", + "ArrayBuffer", + "SharedArrayBuffer", + "ArrayBufferView", + "ReadableStream (Web Streams)" + ], + "messages": { + "cordovaInvalidArrayBufferResult": "invalid result types for readAsArrayBuffer: {resultType}", + "nodeStreamBackwardsRead": "cannot slice from position which we already seeked away", + "nodeStreamChunkSizeRequired": "cannot create source for stream without a finite value for the `chunkSize` option; specify a chunkSize to control the memory consumption", + "nodeStreamStartOutsideBuffer": "slice start is outside of buffer (currently not implemented)", + "unsupportedSourceType": "in this environment the source object may only be an instance of: {supportedTypes}", + "webStreamAlreadyLocked": "Readable stream is already locked to reader. tus-js-client cannot obtain a new reader.", + "webStreamBackwardsRead": "Requested data is before the reader's current offset", + "webStreamChunkSizeRequired": "cannot create source for stream without a finite value for the `chunkSize` option", + "webStreamMissingBuffer": "cannot _getDataFromBuffer because _buffer is unset", + "webStreamUnknownDataType": "Unknown data type" + }, + "nodeExtraTypes": [ + "fs.ReadStream (Node.js)", + "stream.Readable (Node.js)" + ] + }, + "fingerprints": { + "browserBlob": { + "fields": [ + "prefix", + "name", + "type", + "size", + "lastModified", + "endpoint" + ], + "prefix": "tus-br", + "separator": "-" + }, + "nodeBuffer": { + "fields": [ + "prefix", + "contentHash", + "size", + "endpoint" + ], + "hashAlgorithm": "md5", + "prefix": "node-buffer", + "sampleBytes": 65536, + "separator": "-" + }, + "nodeFile": { + "conformanceFixture": { + "absolutePath": "/tmp/tus-contract-file.bin", + "mtimeMs": 1700000000123 + }, + "fields": [ + "prefix", + "absolutePath", + "size", + "mtimeMs", + "endpoint" + ], + "path": "absolute", + "prefix": "node-file", + "separator": "-" + }, + "reactNative": { + "emptyName": "noname", + "emptySize": "nosize", + "exifHash": "javascript-string-hash-code", + "fields": [ + "prefix", + "name", + "size", + "exifHash", + "endpoint" + ], + "noExif": "noexif", + "prefix": "tus-rn", + "separator": "/" + }, + "unsupportedInput": "null" + }, + "httpStacks": { + "browserStackNodeReadableBody": "unsupported", + "messages": { + "browserStackNodeReadableBodyUnsupported": "Using a Node.js readable stream as HTTP request body is not supported using the {stackName} HTTP stack.", + "nodeStackMissingStatusCode": "no status code available yet", + "nodeStackUnsupportedBodyType": "Unsupported HTTP request body type in Node.js HTTP stack: {bodyType} (constructor: {constructorName})" + }, + "nodeStackMissingStatusCode": "throw", + "progressThrottle": { + "leading": true, + "milliseconds": 100, + "trailing": false + }, + "nodeStackUnsupportedBodyType": "throw" + }, + "locationResolution": { + "strategy": "relative-to-creation-request-url" + }, + "messages": { + "configuredUploadSizeMismatch": "upload was configured with a size of {expectedSize} bytes, but the source is done after {actualSize} bytes", + "cannotDeriveUploadSize": "tus: cannot automatically derive upload's size from input. Specify it manually using the `uploadSize` option or use the `uploadLengthDeferred` option", + "createMissingEndpoint": "tus: unable to create upload because no endpoint is provided", + "createMissingSize": "tus: expected _size to be set", + "createUploadRequestFailed": "tus: failed to create upload", + "createdUpload": "Created upload at {uploadUrl}", + "finalUploadMissingPartialUrls": "tus: Expected _parallelUploadUrls to be set", + "finalUploadRequestFailed": "tus: failed to concatenate parallel uploads", + "fingerprintCalculated": "Calculated fingerprint: {fingerprint}", + "fingerprintUnavailable": "tus: unable to calculate fingerprint for this input file", + "fingerprintUnavailableForStorage": "No fingerprint was calculated meaning that the upload cannot be stored in the URL storage.", + "invalidUploadSize": "tus: cannot convert `uploadSize` option into a number", + "invalidChunkOffset": "tus: invalid or missing offset value", + "invalidResumeLength": "tus: invalid or missing length value", + "invalidResumeOffset": "tus: invalid Upload-Offset header", + "lockedUpload": "tus: upload is currently locked; retry later", + "nonErrorThrownValue": "tus: value thrown that is not an error: {value}", + "missingEndpointOrUploadUrl": "tus: neither an endpoint or an upload URL is provided", + "missingInput": "tus: no file or stream to upload provided", + "missingPatchUrl": "tus: Expected url to be set", + "missingResumeOffset": "tus: missing Upload-Offset header", + "removedResumeOption": "tus: The `resume` option has been removed in tus-js-client v2. Please use the URL storage API instead.", + "parallelBoundariesLengthMismatch": "tus: the `parallelUploadBoundaries` must have the same length as the value of `parallelUploads`", + "parallelBoundariesWithoutParallelUploads": "tus: cannot use the `parallelUploadBoundaries` option when `parallelUploads` is disabled", + "parallelUploadMissingSize": "tus: Expected _size to be set", + "parallelUploadsWithDeferredLength": "tus: cannot use the `uploadLengthDeferred` option when parallelUploads is enabled", + "parallelUploadsWithUploadDataDuringCreation": "tus: cannot use the `uploadDataDuringCreation` option when parallelUploads is enabled", + "parallelUploadsWithUploadSize": "tus: cannot use the `uploadSize` option when parallelUploads is enabled", + "parallelUploadsWithUploadUrl": "tus: cannot use the `uploadUrl` option when parallelUploads is enabled", + "parallelUploadSliceMissingValue": "tus: no value returned while slicing file for parallel uploads", + "reactNativeUriBlobFetchFailed": "tus: cannot fetch `file.uri` as Blob, make sure the uri is correct and accessible. {error}", + "reactNativeUriUnsupported": "tus: file objects with `uri` property is only supported in React Native", + "resumeUploadRequestFailed": "tus: failed to resume upload", + "resumeWithoutEndpoint": "tus: unable to resume upload (new upload cannot be created without an endpoint)", + "retryDelaysNotArray": "tus: the `retryDelays` option must either be an array or null", + "storageMissingParallelUploadUrls": "tus: cannot store parallel upload because no partial upload URLs are available", + "storageMissingUploadUrl": "tus: cannot store upload because no upload URL is available", + "terminateUploadRequestFailed": "tus: failed to terminate upload", + "unexpectedChunkResponse": "tus: unexpected response while uploading chunk", + "unexpectedCreateResponse": "tus: unexpected response while creating upload", + "unexpectedResumeResponse": "tus: unexpected response while resuming upload", + "unexpectedTerminateResponse": "tus: unexpected response while terminating upload", + "uploadChunkRequestFailed": "tus: failed to upload chunk at offset {offset}", + "uploadLocationMissing": "tus: invalid or missing Location header", + "unsupportedProtocolPrefix": "tus: unsupported protocol " + }, + "minimumParallelUploads": 2, + "optionDefaults": { + "addRequestId": false, + "chunkSize": { + "kind": "unbounded" + }, + "headers": {}, + "metadata": {}, + "metadataForPartialUploads": {}, + "overridePatchMethod": false, + "parallelUploads": 1, + "removeFingerprintOnSuccess": false, + "retryDelays": [ + 0, + 1000, + 3000, + 5000 + ], + "storeFingerprintForResuming": true, + "uploadDataDuringCreation": false, + "uploadLengthDeferred": false + }, + "parallelPartialUpload": { + "headerKind": "partial-upload", + "metadataSource": "metadataForPartialUploads", + "nestedParallelUploads": "disabled", + "urlStorage": "parent-managed" + }, + "parallelUploadCleanup": { + "onPartError": "terminate-created-partials-when-abort-termination-enabled", + "returnedError": "original-error-unless-cleanup-fails" + }, + "parallelUploadExecution": { + "cancelRemainingOnPartError": true, + "resultOrder": "part-index", + "sourceRead": "before-worker-start", + "workerStrategy": "one-worker-per-part" + }, + "parallelUploadSplit": { + "strategy": "contiguous-floor-size-last-remainder" + }, + "requestHeaders": { + "layers": [ + "operation", + "custom", + "request-id" + ], + "requestIdSource": "sdk-generated-uuid" + }, + "requestLifecycle": { + "hooks": { + "afterResponse": "after-successful-transport-response", + "beforeRequest": "before-transport-send" + }, + "retry": { + "attemptCounter": { + "increment": "after-retry-scheduled", + "reset": "when-offset-advanced-since-last-retry" + }, + "customDecision": "custom-callback-before-default-decision", + "delaySource": "retry-delays-indexed-by-attempt", + "defaultDecision": "retryable-status-and-online", + "error": { + "retryableWhen": "request-context-present" + }, + "evaluationTrigger": "generated-plan-evaluate-policy", + "failure": { + "exhaustedDelays": "emit-error", + "nonRetryableError": "emit-error", + "policyRejected": "emit-error" + }, + "onlineSignal": { + "defaultWhenUnavailable": true, + "offlineWhenPlatformOnlineIsFalse": true, + "source": "sdk-platform-online-status" + }, + "timer": { + "restart": "start-upload-after-delay", + "source": "sdk-platform-timer" + } + } + }, + "runtimeSetup": { + "fingerprintOverrideInputKinds": [ + "node-readable-stream", + "web-readable-stream" + ], + "urlStorageEventKinds": [ + "url-storage-add", + "url-storage-find", + "url-storage-remove" + ] + }, + "startValidation": { + "conformanceInputFingerprint": { + "kind": "scenario-id-prefix", + "prefix": "contract-" + }, + "parallelOptionValidationReasons": [ + "parallelUploadsWithDeferredLength", + "parallelUploadsWithUploadDataDuringCreation" + ], + "rules": [ + { + "message": { + "key": "missingInput", + "kind": "client-flow-message" + }, + "predicate": { + "equals": false, + "input": "hasFile", + "kind": "boolean-input" + }, + "reason": "missingInput" + }, + { + "message": { + "input": "protocol", + "key": "unsupportedProtocolPrefix", + "kind": "client-flow-message-with-input-suffix" + }, + "predicate": { + "equals": false, + "input": "protocol", + "kind": "supported-protocol" + }, + "reason": "unsupportedProtocol" + }, + { + "message": { + "key": "missingEndpointOrUploadUrl", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "equals": false, + "input": "hasEndpoint", + "kind": "boolean-input" + }, + { + "equals": false, + "input": "hasUploadUrl", + "kind": "boolean-input" + }, + { + "equals": false, + "input": "hasCurrentUrl", + "kind": "boolean-input" + } + ] + }, + "reason": "missingEndpointOrUploadUrl" + }, + { + "message": { + "key": "retryDelaysNotArray", + "kind": "client-flow-message" + }, + "predicate": { + "equals": false, + "input": "retryDelays", + "kind": "array-or-null" + }, + "reason": "retryDelaysNotArray" + }, + { + "message": { + "key": "parallelUploadsWithUploadUrl", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "input": "parallelUploads", + "kind": "number-input-gte-client-flow-value", + "value": "minimumParallelUploads" + }, + { + "equals": true, + "input": "hasUploadUrl", + "kind": "boolean-input" + } + ] + }, + "reason": "parallelUploadsWithUploadUrl" + }, + { + "message": { + "key": "parallelUploadsWithUploadSize", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "input": "parallelUploads", + "kind": "number-input-gte-client-flow-value", + "value": "minimumParallelUploads" + }, + { + "equals": true, + "input": "hasUploadSize", + "kind": "boolean-input" + } + ] + }, + "reason": "parallelUploadsWithUploadSize" + }, + { + "message": { + "key": "parallelUploadsWithDeferredLength", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "input": "parallelUploads", + "kind": "number-input-gte-client-flow-value", + "value": "minimumParallelUploads" + }, + { + "equals": true, + "input": "uploadLengthDeferred", + "kind": "boolean-input" + } + ] + }, + "reason": "parallelUploadsWithDeferredLength" + }, + { + "message": { + "key": "parallelUploadsWithUploadDataDuringCreation", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "input": "parallelUploads", + "kind": "number-input-gte-client-flow-value", + "value": "minimumParallelUploads" + }, + { + "equals": true, + "input": "uploadDataDuringCreation", + "kind": "boolean-input" + } + ] + }, + "reason": "parallelUploadsWithUploadDataDuringCreation" + }, + { + "message": { + "key": "parallelBoundariesWithoutParallelUploads", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "input": "parallelUploadBoundariesCount", + "kind": "number-input-not-null" + }, + { + "input": "parallelUploads", + "kind": "number-input-lt-client-flow-value", + "value": "minimumParallelUploads" + } + ] + }, + "reason": "parallelBoundariesWithoutParallelUploads" + }, + { + "message": { + "key": "parallelBoundariesLengthMismatch", + "kind": "client-flow-message" + }, + "predicate": { + "kind": "all", + "predicates": [ + { + "input": "parallelUploadBoundariesCount", + "kind": "number-input-not-null" + }, + { + "kind": "number-input-not-equals-number-input", + "left": "parallelUploads", + "right": "parallelUploadBoundariesCount" + } + ] + }, + "reason": "parallelBoundariesLengthMismatch" + } + ] + }, + "urlStorage": { + "id": { + "multiplier": 1000000000000, + "strategy": "rounded-random-number" + }, + "messages": { + "missingItem": "didn't find item for key {key}", + "missingKey": "didn't find key for item {index}" + }, + "namespace": "tus", + "record": { + "creationTime": "sdk-current-date-string", + "fields": { + "creationTime": "creationTime", + "metadata": "metadata", + "size": "size", + "uploadUrl": "uploadUrl", + "urlStorageKey": "urlStorageKey" + }, + "missingUrl": "fail", + "storedUrlKind": "single-or-parallel-upload-url" + }, + "removeOnSuccess": "when-option-enabled", + "separator": "::", + "webStorage": { + "malformedEntry": "ignore", + "probeKey": "tusSupport", + "unavailableDomExceptionNames": [ + "QuotaExceededError", + "SecurityError" + ] + } + } + }, + "clientProtocol": { + "compatibilityVersions": [ + { + "protocolVersion": "tus-v1", + "requiresKnownUploadLengthOnOffsetResponse": true, + "requestHeaders": [ + { + "headerName": "Tus-Resumable", + "value": "1.0.0" + } + ], + "responseHeaders": [ + { + "headerName": "Tus-Resumable", + "value": "1.0.0" + } + ], + "uploadBodyContentType": "application/offset+octet-stream" + }, + { + "protocolVersion": "ietf-draft-03", + "requestHeaders": [ + { + "headerName": "Upload-Draft-Interop-Version", + "value": "5" + } + ], + "responseHeaders": [], + "uploadCompleteHeader": { + "completeValue": "?1", + "headerName": "Upload-Complete", + "incompleteValue": "?0" + } + }, + { + "protocolVersion": "ietf-draft-05", + "requestHeaders": [ + { + "headerName": "Upload-Draft-Interop-Version", + "value": "6" + } + ], + "responseHeaders": [], + "uploadBodyContentType": "application/partial-upload", + "uploadCompleteHeader": { + "completeValue": "?1", + "headerName": "Upload-Complete", + "incompleteValue": "?0" + } + } + ], + "concatenation": { + "extensionName": "concatenation", + "finalPrefix": "final;", + "headerName": "Upload-Concat", + "partialValue": "partial", + "uploadUrlSeparator": " " + }, + "locationHeaderName": "Location", + "metadataEncoding": { + "entrySeparator": ",", + "keyValueSeparator": " ", + "valueEncoding": "base64" + }, + "metadataHeaderName": "Upload-Metadata", + "methodOverrides": [ + { + "headerName": "X-HTTP-Method-Override", + "headerValue": "PATCH", + "inputFlag": "overridePatchMethod", + "method": "POST", + "operationId": "patchTusUpload" + } + ], + "requestIdHeaderName": "X-Request-ID", + "retryPolicy": { + "clientErrorStatusCategory": 400, + "lockedStatusCode": 423, + "retryableClientStatusCodes": [ + 409, + 423 + ], + "successStatusCategory": 200 + }, + "serverCapabilityExtensions": { + "creation": "creation", + "termination": "termination" + }, + "uploadBody": { + "contentTypeHeaderName": "Content-Type" + }, + "uploadLength": { + "deferLengthHeaderName": "Upload-Defer-Length", + "deferLengthHeaderValue": "1", + "lengthHeaderName": "Upload-Length" + }, + "uploadOffsetHeaderName": "Upload-Offset" + }, + "clientUrlStorageConformanceScenarios": [ + { + "actions": [ + { + "kind": "assert-empty" + }, + { + "expectedKeyPrefix": "tus::contract-storage-a::", + "fingerprint": "contract-storage-a", + "keyRef": "a1", + "kind": "add-upload", + "upload": { + "id": 1, + "metadata": { + "filename": "a1.txt" + }, + "size": 11, + "uploadUrl": "https://tus.io/uploads/storage-a1" + } + }, + { + "expectedKeyPrefix": "tus::contract-storage-a::", + "fingerprint": "contract-storage-a", + "keyRef": "a2", + "kind": "add-upload", + "upload": { + "id": 2, + "metadata": { + "filename": "a2.txt" + }, + "size": 12, + "uploadUrl": "https://tus.io/uploads/storage-a2" + } + }, + { + "expectedKeyPrefix": "tus::contract-storage-b::", + "fingerprint": "contract-storage-b", + "keyRef": "b1", + "kind": "add-upload", + "upload": { + "id": 3, + "metadata": { + "filename": "b1.txt" + }, + "size": 13, + "uploadUrl": "https://tus.io/uploads/storage-b1" + } + }, + { + "expectedKeyRefs": [ + "a1", + "a2" + ], + "fingerprint": "contract-storage-a", + "kind": "find-by-fingerprint" + }, + { + "expectedKeyRefs": [ + "b1" + ], + "fingerprint": "contract-storage-b", + "kind": "find-by-fingerprint" + }, + { + "expectedKeyRefs": [ + "a1", + "a2", + "b1" + ], + "kind": "find-all" + }, + { + "keyRef": "a2", + "kind": "remove-upload" + }, + { + "keyRef": "b1", + "kind": "remove-upload" + }, + { + "expectedKeyRefs": [ + "a1" + ], + "fingerprint": "contract-storage-a", + "kind": "find-by-fingerprint" + }, + { + "expectedKeyRefs": [], + "fingerprint": "contract-storage-b", + "kind": "find-by-fingerprint" + } + ], + "backend": "web-storage", + "featureId": "urlStorageBackends", + "runtimes": [ + "browser" + ], + "scenarioId": "webStorageUrlStorageBackend" + }, + { + "actions": [ + { + "kind": "assert-empty" + }, + { + "expectedKeyPrefix": "tus::contract-storage-a::", + "fingerprint": "contract-storage-a", + "keyRef": "a1", + "kind": "add-upload", + "upload": { + "id": 1, + "metadata": { + "filename": "a1.txt" + }, + "size": 11, + "uploadUrl": "https://tus.io/uploads/storage-a1" + } + }, + { + "expectedKeyPrefix": "tus::contract-storage-a::", + "fingerprint": "contract-storage-a", + "keyRef": "a2", + "kind": "add-upload", + "upload": { + "id": 2, + "metadata": { + "filename": "a2.txt" + }, + "size": 12, + "uploadUrl": "https://tus.io/uploads/storage-a2" + } + }, + { + "expectedKeyPrefix": "tus::contract-storage-b::", + "fingerprint": "contract-storage-b", + "keyRef": "b1", + "kind": "add-upload", + "upload": { + "id": 3, + "metadata": { + "filename": "b1.txt" + }, + "size": 13, + "uploadUrl": "https://tus.io/uploads/storage-b1" + } + }, + { + "expectedKeyRefs": [ + "a1", + "a2" + ], + "fingerprint": "contract-storage-a", + "kind": "find-by-fingerprint" + }, + { + "expectedKeyRefs": [ + "b1" + ], + "fingerprint": "contract-storage-b", + "kind": "find-by-fingerprint" + }, + { + "expectedKeyRefs": [ + "a1", + "a2", + "b1" + ], + "kind": "find-all" + }, + { + "keyRef": "a2", + "kind": "remove-upload" + }, + { + "keyRef": "b1", + "kind": "remove-upload" + }, + { + "expectedKeyRefs": [ + "a1" + ], + "fingerprint": "contract-storage-a", + "kind": "find-by-fingerprint" + }, + { + "expectedKeyRefs": [], + "fingerprint": "contract-storage-b", + "kind": "find-by-fingerprint" + } + ], + "backend": "file-storage", + "featureId": "urlStorageBackends", + "runtimes": [ + "deno", + "node" + ], + "scenarioId": "fileUrlStorageBackend" + } + ], + "managedUpload": { + "capabilities": { + "cleanup": { + "policies": [ + "absent-after-source-unavailable", + "remove-owned-source-after-success", + "remove-owned-source-after-cancel", + "retain-owned-source-while-deferred", + "retain-owned-source-after-permanent-failure", + "retain-source-after-retryable-failure", + "remove-managed-state-after-terminal-retention" + ] + }, + "failureClassification": { + "permanentFailures": [ + "source-unavailable", + "unretryable-protocol-error", + "retry-policy-exhausted" + ], + "retryableFailures": [ + "retryable-protocol-error", + "io-error", + "network-unavailable" + ] + }, + "networkConstraints": { + "options": [ + "any-network", + "unmetered-network" + ] + }, + "retryPolicy": { + "controls": [ + "max-attempts", + "deadline", + "progress-sensitive-budget", + "unbounded-until-permanent-failure" + ], + "permanentFailure": "stop-without-retry", + "progressReset": "reset-budget-after-accepted-offset-advances" + }, + "scheduling": { + "strategies": [ + "foreground-task", + "process-lifetime-worker-pool", + "durable-os-scheduler" + ] + }, + "sourceDurability": { + "ownedCopyCleanup": "after-success-or-cancel", + "strategies": [ + "copy-to-owned-storage", + "reference-original-source", + "memory-only" + ] + }, + "stateReporting": { + "states": [ + "pending", + "running", + "succeeded", + "failed" + ], + "terminalRetention": "session-and-next-launch", + "transientRetention": "until-terminal" + } + }, + "conformance": { + "scenarioIds": [ + "managedUploadDurableRetry", + "managedUploadPermanentFailure", + "managedUploadRetryPolicyExhausted", + "managedUploadSourceUnavailable", + "managedUploadNetworkConstraint" + ], + "status": "covered-by-generated-scenario" + }, + "description": "Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.", + "featureId": "managedUpload", + "flow": [ + { + "kind": "managed-primitive", + "primitive": "accept-upload-submission", + "summary": "Accept source, metadata, headers, endpoint, and retry/scheduling policy." + }, + { + "kind": "managed-primitive", + "primitive": "make-source-durable", + "summary": "Keep the source readable according to the selected runtime durability strategy." + }, + { + "kind": "managed-primitive", + "primitive": "schedule-upload-work", + "summary": "Run upload work according to the runtime scheduler capability." + }, + { + "featureId": "singleUploadLifecycle", + "kind": "protocol-feature", + "summary": "Use the raw protocol upload lifecycle for each execution attempt." + }, + { + "featureId": "retryOffsetRecovery", + "kind": "protocol-feature", + "summary": "Use protocol retry and offset recovery before classifying terminal failure." + }, + { + "kind": "managed-primitive", + "primitive": "publish-upload-state", + "summary": "Expose pending, running, succeeded, and failed state snapshots." + }, + { + "kind": "managed-primitive", + "primitive": "cleanup-managed-upload", + "summary": "Remove owned sources and terminal state according to cleanup policy." + } + ], + "layer": "feature-over-protocol", + "primitives": [ + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload" + ], + "protocolPrimitives": [ + "store-resume-url", + "resume-from-previous-upload", + "recover-offset-after-error", + "retry-with-backoff", + "emit-progress", + "emit-chunk-complete", + "terminate-upload" + ], + "runtimeProfiles": [ + { + "networkConstraints": [ + "any-network", + "unmetered-network" + ], + "runtime": "android", + "scheduler": "durable-os-scheduler", + "sourceDurability": [ + "copy-to-owned-storage", + "reference-original-source" + ], + "stateBackend": "platform-key-value-store", + "transportProfileId": "java-http-url-connection" + }, + { + "networkConstraints": [ + "any-network", + "unmetered-network" + ], + "runtime": "ios", + "scheduler": "durable-os-scheduler", + "sourceDurability": [ + "copy-to-owned-storage", + "reference-original-source" + ], + "stateBackend": "platform-key-value-store" + }, + { + "networkConstraints": [ + "any-network" + ], + "runtime": "browser", + "scheduler": "foreground-task", + "sourceDurability": [ + "reference-original-source", + "memory-only" + ], + "stateBackend": "web-storage" + }, + { + "networkConstraints": [ + "any-network" + ], + "runtime": "java", + "scheduler": "process-lifetime-worker-pool", + "sourceDurability": [ + "copy-to-owned-storage", + "reference-original-source" + ], + "stateBackend": "filesystem", + "transportProfileId": "java-http-url-connection" + }, + { + "networkConstraints": [ + "any-network" + ], + "runtime": "node", + "scheduler": "process-lifetime-worker-pool", + "sourceDurability": [ + "copy-to-owned-storage", + "reference-original-source", + "memory-only" + ], + "stateBackend": "filesystem" + }, + { + "networkConstraints": [ + "any-network" + ], + "runtime": "react-native", + "scheduler": "foreground-task", + "sourceDurability": [ + "reference-original-source", + "memory-only" + ], + "stateBackend": "platform-key-value-store" + } + ], + "scenarios": [ + { + "proofs": [ + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "afterAcceptedOffset": 7, + "kind": "io-error", + "phase": "after-accepted-offset" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": { + "Location": "https://tus.io/uploads/managed-durable-retry" + }, + "statusCode": 201 + }, + "url": "endpoint" + }, + { + "bodySize": 7, + "headers": { + "Upload-Offset": "0" + }, + "operationId": "patchTusUpload", + "response": { + "headers": { + "Upload-Offset": "7" + }, + "statusCode": 204 + }, + "url": "upload" + } + ], + "stateAfterAttempt": "failed" + }, + { + "attemptIndex": 1, + "requests": [ + { + "headers": {}, + "operationId": "getTusUploadOffset", + "response": { + "headers": { + "Upload-Length": "14", + "Upload-Offset": "7" + }, + "statusCode": 200 + }, + "url": "upload" + }, + { + "bodySize": 7, + "headers": { + "Upload-Offset": "7" + }, + "operationId": "patchTusUpload", + "response": { + "headers": { + "Upload-Offset": "14" + }, + "statusCode": 204 + }, + "url": "upload" + } + ], + "stateAfterAttempt": "succeeded" + } + ], + "cleanup": { + "ownedSource": "remove-owned-source-after-success", + "resumeUrl": "remove-after-success" + }, + "input": { + "chunkSize": 7, + "content": "hello managed!", + "fingerprint": "managed-durable-retry-fingerprint", + "metadata": { + "filename": "managed.txt" + }, + "uploadPath": "managed-durable-retry" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "kind": "terminal", + "state": "succeeded" + }, + "retryDelays": [ + 0 + ], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed", + "running", + "succeeded" + ], + "runtime": "java", + "scheduler": "process-lifetime-worker-pool", + "stateBackend": "filesystem" + }, + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "afterAcceptedOffset": 7, + "kind": "io-error", + "phase": "after-accepted-offset" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": { + "Location": "https://tus.io/uploads/managed-durable-retry" + }, + "statusCode": 201 + }, + "url": "endpoint" + }, + { + "bodySize": 7, + "headers": { + "Upload-Offset": "0" + }, + "operationId": "patchTusUpload", + "response": { + "headers": { + "Upload-Offset": "7" + }, + "statusCode": 204 + }, + "url": "upload" + } + ], + "stateAfterAttempt": "failed" + }, + { + "attemptIndex": 1, + "requests": [ + { + "headers": {}, + "operationId": "getTusUploadOffset", + "response": { + "headers": { + "Upload-Length": "14", + "Upload-Offset": "7" + }, + "statusCode": 200 + }, + "url": "upload" + }, + { + "bodySize": 7, + "headers": { + "Upload-Offset": "7" + }, + "operationId": "patchTusUpload", + "response": { + "headers": { + "Upload-Offset": "14" + }, + "statusCode": 204 + }, + "url": "upload" + } + ], + "stateAfterAttempt": "succeeded" + } + ], + "cleanup": { + "ownedSource": "remove-owned-source-after-success", + "resumeUrl": "remove-after-success" + }, + "input": { + "chunkSize": 7, + "content": "hello managed!", + "fingerprint": "managed-durable-retry-fingerprint", + "metadata": { + "filename": "managed.txt" + }, + "uploadPath": "managed-durable-retry" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "kind": "terminal", + "state": "succeeded" + }, + "retryDelays": [ + 0 + ], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed", + "running", + "succeeded" + ], + "runtime": "android", + "scheduler": "durable-os-scheduler", + "stateBackend": "platform-key-value-store" + } + ], + "requiredPrimitives": [ + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "publish-upload-state", + "cleanup-managed-upload" + ], + "scenarioId": "managedUploadDurableRetry", + "summary": "Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup." + }, + { + "proofs": [ + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "kind": "unretryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 400 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + } + ], + "cleanup": { + "ownedSource": "retain-owned-source-after-permanent-failure", + "resumeUrl": "absent-after-permanent-failure" + }, + "input": { + "chunkSize": 7, + "content": "hello failure!", + "fingerprint": "managed-permanent-failure-fingerprint", + "metadata": { + "filename": "managed-permanent-failure.txt" + }, + "uploadPath": "managed-permanent-failure" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "failure": "unretryable-protocol-error", + "kind": "terminal", + "state": "failed" + }, + "retryDelays": [], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed" + ], + "runtime": "java", + "scheduler": "process-lifetime-worker-pool", + "stateBackend": "filesystem" + }, + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "kind": "unretryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 400 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + } + ], + "cleanup": { + "ownedSource": "retain-owned-source-after-permanent-failure", + "resumeUrl": "absent-after-permanent-failure" + }, + "input": { + "chunkSize": 7, + "content": "hello failure!", + "fingerprint": "managed-permanent-failure-fingerprint", + "metadata": { + "filename": "managed-permanent-failure.txt" + }, + "uploadPath": "managed-permanent-failure" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "failure": "unretryable-protocol-error", + "kind": "terminal", + "state": "failed" + }, + "retryDelays": [], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed" + ], + "runtime": "android", + "scheduler": "durable-os-scheduler", + "stateBackend": "platform-key-value-store" + } + ], + "requiredPrimitives": [ + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload" + ], + "scenarioId": "managedUploadPermanentFailure", + "summary": "Classify unretryable protocol failures as terminal without further retry." + }, + { + "proofs": [ + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "kind": "retryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 500 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + }, + { + "attemptIndex": 1, + "failure": { + "kind": "retryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 500 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + }, + { + "attemptIndex": 2, + "failure": { + "kind": "retryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 500 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + } + ], + "cleanup": { + "ownedSource": "retain-owned-source-after-permanent-failure", + "resumeUrl": "absent-after-permanent-failure" + }, + "input": { + "chunkSize": 7, + "content": "hello retries!", + "fingerprint": "managed-retry-exhausted-fingerprint", + "metadata": { + "filename": "managed-retry-exhausted.txt" + }, + "uploadPath": "managed-retry-exhausted" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "failure": "retry-policy-exhausted", + "kind": "terminal", + "state": "failed" + }, + "retryDelays": [ + 0, + 0 + ], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed", + "running", + "failed", + "running", + "failed" + ], + "runtime": "java", + "scheduler": "process-lifetime-worker-pool", + "stateBackend": "filesystem" + }, + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "kind": "retryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 500 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + }, + { + "attemptIndex": 1, + "failure": { + "kind": "retryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 500 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + }, + { + "attemptIndex": 2, + "failure": { + "kind": "retryable-protocol-error", + "phase": "during-protocol-request" + }, + "requests": [ + { + "bodySize": 0, + "headers": { + "Upload-Length": "14" + }, + "operationId": "createTusUpload", + "response": { + "headers": {}, + "statusCode": 500 + }, + "url": "endpoint" + } + ], + "stateAfterAttempt": "failed" + } + ], + "cleanup": { + "ownedSource": "retain-owned-source-after-permanent-failure", + "resumeUrl": "absent-after-permanent-failure" + }, + "input": { + "chunkSize": 7, + "content": "hello retries!", + "fingerprint": "managed-retry-exhausted-fingerprint", + "metadata": { + "filename": "managed-retry-exhausted.txt" + }, + "uploadPath": "managed-retry-exhausted" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "failure": "retry-policy-exhausted", + "kind": "terminal", + "state": "failed" + }, + "retryDelays": [ + 0, + 0 + ], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed", + "running", + "failed", + "running", + "failed" + ], + "runtime": "android", + "scheduler": "durable-os-scheduler", + "stateBackend": "platform-key-value-store" + } + ], + "requiredPrimitives": [ + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload" + ], + "scenarioId": "managedUploadRetryPolicyExhausted", + "summary": "Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed." + }, + { + "proofs": [ + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "kind": "source-unavailable", + "phase": "before-protocol-request" + }, + "requests": [], + "stateAfterAttempt": "failed" + } + ], + "cleanup": { + "ownedSource": "absent-after-source-unavailable", + "resumeUrl": "absent-after-permanent-failure" + }, + "input": { + "chunkSize": 7, + "content": "hello missing!", + "fingerprint": "managed-source-unavailable-fingerprint", + "metadata": { + "filename": "managed-source-unavailable.txt" + }, + "uploadPath": "managed-source-unavailable" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "failure": "source-unavailable", + "kind": "terminal", + "state": "failed" + }, + "retryDelays": [], + "sourceAvailability": "missing-before-durable-copy", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed" + ], + "runtime": "java", + "scheduler": "process-lifetime-worker-pool", + "stateBackend": "filesystem" + }, + { + "attempts": [ + { + "attemptIndex": 0, + "failure": { + "kind": "source-unavailable", + "phase": "before-protocol-request" + }, + "requests": [], + "stateAfterAttempt": "failed" + } + ], + "cleanup": { + "ownedSource": "absent-after-source-unavailable", + "resumeUrl": "absent-after-permanent-failure" + }, + "input": { + "chunkSize": 7, + "content": "hello missing!", + "fingerprint": "managed-source-unavailable-fingerprint", + "metadata": { + "filename": "managed-source-unavailable.txt" + }, + "uploadPath": "managed-source-unavailable" + }, + "network": { + "current": "unmetered-network", + "decision": "start-upload-work", + "required": "any-network" + }, + "outcome": { + "failure": "source-unavailable", + "kind": "terminal", + "state": "failed" + }, + "retryDelays": [], + "sourceAvailability": "missing-before-durable-copy", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending", + "running", + "failed" + ], + "runtime": "android", + "scheduler": "durable-os-scheduler", + "stateBackend": "platform-key-value-store" + } + ], + "requiredPrimitives": [ + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload" + ], + "scenarioId": "managedUploadSourceUnavailable", + "summary": "Classify source disappearance before protocol requests as terminal without issuing a TUS request." + }, + { + "proofs": [ + { + "attempts": [], + "cleanup": { + "ownedSource": "retain-owned-source-while-deferred", + "resumeUrl": "absent-while-deferred" + }, + "input": { + "chunkSize": 7, + "content": "hello later!", + "fingerprint": "managed-network-constraint-fingerprint", + "metadata": { + "filename": "managed-network-constraint.txt" + }, + "uploadPath": "managed-network-constraint" + }, + "network": { + "current": "metered-network", + "decision": "defer-until-network-constraint-satisfied", + "required": "unmetered-network" + }, + "outcome": { + "kind": "deferred", + "reason": "network-constraint-unsatisfied", + "state": "pending" + }, + "retryDelays": [], + "sourceAvailability": "available", + "sourceDurability": "copy-to-owned-storage", + "states": [ + "pending" + ], + "runtime": "android", + "scheduler": "durable-os-scheduler", + "stateBackend": "platform-key-value-store" + } + ], + "requiredPrimitives": [ + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "publish-upload-state" + ], + "scenarioId": "managedUploadNetworkConstraint", + "summary": "Honor network constraints before starting or resuming upload work." + } + ] + }, + "operations": [ + { + "operationId": "discoverTusCapabilities", + "role": "capability-discovery", + "method": "OPTIONS", + "path": "/resumable/files/", + "request": { + "bodyKind": "empty", + "contentType": null, + "headerVariants": [] + }, + "responses": [ + { + "statusCode": 200, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Extension", + "name": "tus-extension", + "required": true + }, + { + "displayName": "Tus-Max-Size", + "name": "tus-max-size", + "required": true + }, + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Tus-Version", + "name": "tus-version", + "required": true + } + ] + } + ] + } + ] + }, + { + "operationId": "createTusUpload", + "role": "creation", + "method": "POST", + "path": "/resumable/files/", + "request": { + "bodyKind": "empty", + "contentType": null, + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Length", + "name": "upload-length", + "required": true + }, + { + "displayName": "Upload-Metadata", + "name": "upload-metadata", + "required": true + } + ] + }, + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Defer-Length", + "name": "upload-defer-length", + "required": true + }, + { + "displayName": "Upload-Metadata", + "name": "upload-metadata", + "required": true + } + ] + }, + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Concat", + "name": "upload-concat", + "required": true + }, + { + "displayName": "Upload-Length", + "name": "upload-length", + "required": true + }, + { + "displayName": "Upload-Metadata", + "name": "upload-metadata", + "required": false + } + ] + }, + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Concat", + "name": "upload-concat", + "required": true + }, + { + "displayName": "Upload-Metadata", + "name": "upload-metadata", + "required": false + } + ] + } + ] + }, + "responses": [ + { + "statusCode": 201, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Location", + "name": "location", + "required": true + }, + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + }, + { + "statusCode": 413, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + }, + { + "statusCode": 500, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + } + ] + }, + { + "operationId": "getTusUploadOffset", + "role": "offset-discovery", + "method": "HEAD", + "path": "/resumable/files/{upload_id}", + "request": { + "bodyKind": "empty", + "contentType": null, + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + }, + "responses": [ + { + "statusCode": 200, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Length", + "name": "upload-length", + "required": true + }, + { + "displayName": "Upload-Offset", + "name": "upload-offset", + "required": true + } + ] + }, + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Defer-Length", + "name": "upload-defer-length", + "required": true + }, + { + "displayName": "Upload-Offset", + "name": "upload-offset", + "required": true + } + ] + } + ] + } + ] + }, + { + "operationId": "patchTusUpload", + "role": "upload-chunk", + "method": "PATCH", + "path": "/resumable/files/{upload_id}", + "request": { + "bodyKind": "binary", + "contentType": "application/offset+octet-stream", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Content-Type", + "name": "content-type", + "required": true + }, + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Offset", + "name": "upload-offset", + "required": true + } + ] + } + ] + }, + "responses": [ + { + "statusCode": 204, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + }, + { + "displayName": "Upload-Offset", + "name": "upload-offset", + "required": true + } + ] + } + ] + }, + { + "statusCode": 500, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + } + ] + }, + { + "operationId": "terminateTusUpload", + "role": "termination", + "method": "DELETE", + "path": "/resumable/files/{upload_id}", + "request": { + "bodyKind": "empty", + "contentType": null, + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + }, + "responses": [ + { + "statusCode": 204, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + }, + { + "statusCode": 400, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + }, + { + "statusCode": 423, + "bodyKind": "empty", + "headerVariants": [ + { + "fields": [ + { + "displayName": "Tus-Resumable", + "name": "tus-resumable", + "required": true + } + ] + } + ] + } + ] + }, + { + "operationId": "downloadTusUpload", + "role": "download", + "method": "GET", + "path": "/resumable/files/{upload_id}", + "request": { + "bodyKind": "empty", + "contentType": null, + "headerVariants": [] + }, + "responses": [ + { + "statusCode": 200, + "bodyKind": "binary", + "headerVariants": [] + } + ] + } + ], + "versions": [ + { + "constantName": "PROTOCOL_TUS_V1", + "default": true, + "value": "tus-v1" + }, + { + "constantName": "PROTOCOL_IETF_DRAFT_03", + "value": "ietf-draft-03" + }, + { + "constantName": "PROTOCOL_IETF_DRAFT_05", + "value": "ietf-draft-05" + } + ], + "wireVersions": [ + { + "default": true, + "value": "1.0.0" + } + ] +}