diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 866e680..74472b2 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: 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..61cf2fa --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/Api2DevdockTusUploadExampleTest.java @@ -0,0 +1,307 @@ +package io.tus.android.client; + +import android.app.Activity; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.SharedPreferences; +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.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 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.LinkedHashMap; +import java.util.Map; + +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 { + private static final String PROVIDER_AUTHORITY = "io.tus.android.client.api2devdock"; + + @Test + public void uploadsAndroidContentUriToTransloaditAssembly() throws Exception { + System.setProperty("http.strictPostRedirect", "true"); + + final String scenarioPath = 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 = loadScenario(scenarioPath); + final JSONObject createResponse = + scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final Activity activity = Robolectric.setupActivity(Activity.class); + final String uploadUrl = uploadWithTus(activity, scenario, createResponse); + writeResult(uploadUrl); + + 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 = scenarioBytes(uploadConfig); + final Uri uri = registerContentUri(activity, content); + + final SharedPreferences preferences = activity.getSharedPreferences( + "api2-devdock-tus-upload", + 0 + ); + preferences.edit().clear().commit(); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(new URL(scalarString( + resolveValue(uploadConfig.getJSONObject("tusUrl"), scenario, createResponse) + ))); + client.enableResuming(new TusPreferencesURLStore(preferences)); + client.enableRemoveFingerprintOnSuccess(); + + final TusAndroidUpload upload = new TusAndroidUpload(uri, activity); + upload.setFingerprint(scenario.getString("scenarioId") + "-android-devdock-example"); + upload.setMetadata(uploadMetadata(uploadConfig, scenario, createResponse)); + + 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 Uri registerContentUri(Activity activity, byte[] content) throws IOException { + final File source = new File(activity.getCacheDir(), "api2-devdock-upload.txt"); + Files.write(source.toPath(), content); + + final Uri uri = Uri.parse("content://" + PROVIDER_AUTHORITY + "/api2-devdock-upload.txt"); + 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; + } + + private 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)); + } + + private 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; + } + + private static boolean isRequired() { + return "true".equals(System.getProperty("api2DevdockTusUpload.required")); + } + + private static void writeResult(String uploadUrl) throws IOException, JSONException { + final String resultPath = System.getenv("API2_SDK_EXAMPLE_RESULT"); + if (resultPath == null || resultPath.isEmpty()) { + return; + } + + final JSONObject result = new JSONObject(); + result.put("uploadUrl", uploadUrl); + Files.write( + Paths.get(resultPath), + (result.toString(2) + "\n").getBytes(StandardCharsets.UTF_8) + ); + } + + private 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); + } + + 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 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 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 String scalarString(Object value) { + if (JSONObject.NULL.equals(value)) { + return "null"; + } + + return String.valueOf(value); + } + + 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/GeneratedTusClientConformanceScenarios.java b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusClientConformanceScenarios.java new file mode 100644 index 0000000..3694e2a --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusClientConformanceScenarios.java @@ -0,0 +1,1348 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.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..c99cd8b --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContract.java @@ -0,0 +1,1687 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.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 GeneratedTusProtocolOperation( + "getTusUploadOffset", + "offset-discovery", + "HEAD", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Defer-Length", + "upload-defer-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "patchTusUpload", + "upload-chunk", + "PATCH", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "binary", + "application/offset+octet-stream", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Content-Type", + "content-type", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 204, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "terminateTusUpload", + "termination", + "DELETE", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 204, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "downloadTusUpload", + "download", + "GET", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[0] + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "binary", + new GeneratedTusHeaderVariant[0] + ), + } + ), + }; + + static final 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..20e4bb2 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/GeneratedTusProtocolContractTest.java @@ -0,0 +1,75 @@ +package io.tus.android.client; + +import android.app.Activity; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +import java.net.URL; + +import static org.junit.Assert.assertEquals; + +@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); + } + } + } + + 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..1138489 --- /dev/null +++ b/tus-android-client/src/test/java/io/tus/android/client/TestGeneratedTusManagedUploadRuntime.java @@ -0,0 +1,1826 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.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, + 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, + 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, + 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( + false, + 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( + false, + 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( + false, + 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, + 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, + 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.attempts[0]; + 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; + + 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; + } + } + + 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 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.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 boolean sourceUnavailableBeforeProtocol; + + GeneratedTusManagedUploadSourceExecution( + boolean prepareDurableSourceBeforeProtocol, + boolean simulateMissingSourceBeforeDurableCopy, + boolean sourceUnavailableBeforeProtocol) { + this.prepareDurableSourceBeforeProtocol = prepareDurableSourceBeforeProtocol; + this.simulateMissingSourceBeforeDurableCopy = simulateMissingSourceBeforeDurableCopy; + 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; + } + } + +}