diff --git a/.sdkharness/README.md b/.sdkharness/README.md new file mode 100644 index 000000000..fc2493412 --- /dev/null +++ b/.sdkharness/README.md @@ -0,0 +1,40 @@ +# SDK harness contract + +`tests.tsv` is the versioned contract read by Backblaze's centralized SDK quality harness +(`sdkharness` in `backblaze-labs/demand-side-ai`). This repository owns the executable +assertions in `tests/`; the harness builds this checkout, starts a fresh local B2 simulator, +and retains and reports the evidence. + +One scenario so far: `health/golden-path`, the customer golden path. `tests/run-health` runs +`tests/health/GoldenPath.java` as a single-file Java program that authorizes, uploads a small +file, downloads it and compares the bytes, lists it, deletes it and confirms it is gone, then +prints one result line: + +```text +SDKHARNESS_RESULT health golden-path PASS - +``` + +## What the check can and cannot reach + +- **Only the loopback simulator.** `run-health` refuses any `SDKHARNESS_SIMULATOR_URL` that + is not `http://127.0.0.1:`. The SDK's HTTP support for test servers + (`HttpClientFactoryImpl.Builder.setSupportInsecureHttp`) is enabled for that reason only. +- **Only the simulator's fixed test credential** (`test-key-id` / `test-key`, in the source). + Every ambient `B2_*` variable except `B2_BUCKET_NAME` is dropped first. +- **No build.** The harness builds the `core` and `httpclient` jars from this checkout and + passes them, with their runtime dependencies, as `SDKHARNESS_JAVA_CLASSPATH`. The build + files here are not changed. A missing `java` on `PATH` reports `SKIP`, which the harness + counts as a failure. + +## Run it by hand + +Requires JDK 11 or later and a simulator (`backblaze-labs/b2-simulator`) with a bucket named +`sdkharness-healthcheck`: + +```bash +./gradlew :b2-sdk-httpclient:jar +SDKHARNESS_TEST_LEVEL=health SDKHARNESS_SCENARIO=golden-path \ +SDKHARNESS_SIMULATOR_URL=http://127.0.0.1: B2_BUCKET_NAME=sdkharness-healthcheck \ +SDKHARNESS_JAVA_CLASSPATH=:: \ + .sdkharness/tests/run-health +``` diff --git a/.sdkharness/tests.tsv b/.sdkharness/tests.tsv new file mode 100644 index 000000000..e57624581 --- /dev/null +++ b/.sdkharness/tests.tsv @@ -0,0 +1,2 @@ +test_level scenario target executable +health golden-path simulator ./.sdkharness/tests/run-health diff --git a/.sdkharness/tests/health/GoldenPath.java b/.sdkharness/tests/health/GoldenPath.java new file mode 100644 index 000000000..0557852f0 --- /dev/null +++ b/.sdkharness/tests/health/GoldenPath.java @@ -0,0 +1,101 @@ +// Copyright 2026, Backblaze Inc. All Rights Reserved. +// License https://www.backblaze.com/using_b2_code.html + +import com.backblaze.b2.client.B2ClientConfig; +import com.backblaze.b2.client.B2StorageClient; +import com.backblaze.b2.client.contentHandlers.B2ContentMemoryWriter; +import com.backblaze.b2.client.contentSources.B2ByteArrayContentSource; +import com.backblaze.b2.client.contentSources.B2ContentTypes; +import com.backblaze.b2.client.structures.B2Bucket; +import com.backblaze.b2.client.structures.B2FileVersion; +import com.backblaze.b2.client.structures.B2ListFileNamesRequest; +import com.backblaze.b2.client.structures.B2ListFileVersionsRequest; +import com.backblaze.b2.client.structures.B2UploadFileRequest; +import com.backblaze.b2.client.webApiHttpClient.B2StorageHttpClientBuilder; +import com.backblaze.b2.client.webApiHttpClient.HttpClientFactoryImpl; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.UUID; + +/** + * Customer golden path against the sdkharness simulator: authorize, upload, byte-verified + * download, list, delete, and confirm the file is gone. Run by ../run-health, which checks + * the simulator URL and supplies the classpath. Prints exactly one verdict line: + * {@code GOLDEN-PATH PASS} or {@code GOLDEN-PATH FAIL : }. + */ +public class GoldenPath { + // The simulator's fixed test credential. No other credential is ever used. + private static final String KEY_ID = "test-key-id"; + private static final String KEY = "test-key"; + + private static String step = "configuration"; + + public static void main(String[] args) { + final String masterUrl = System.getenv("SDKHARNESS_SIMULATOR_URL"); + final String bucketName = System.getenv("B2_BUCKET_NAME"); + if (masterUrl == null || bucketName == null || bucketName.isEmpty()) { + fail("simulator URL or bucket name is missing"); + } + final B2ClientConfig config = B2ClientConfig.builder(KEY_ID, KEY, "b2-sdk-java-sdkharness-health") + .setMasterUrl(masterUrl) + .build(); + final String name = "sdkharness-health-check/" + UUID.randomUUID() + ".txt"; + final byte[] payload = ("b2-sdk-java health check " + name).getBytes(StandardCharsets.UTF_8); + + // The simulator speaks plain HTTP on loopback (run-health refuses any other URL), so this + // enables the SDK's documented test-environment switch; everything else is the default client. + try (B2StorageClient client = B2StorageHttpClientBuilder.builder(config) + .setHttpClientFactory(HttpClientFactoryImpl.builder().setSupportInsecureHttp(true).build()) + .build()) { + step = "authorize"; + client.getAccountAuthorization(); + + step = "bucket"; + final B2Bucket bucket = client.getBucketOrNullByName(bucketName); + if (bucket == null) { + fail("bucket " + bucketName + " not found"); + } + final String bucketId = bucket.getBucketId(); + + step = "upload"; + final B2FileVersion uploaded = client.uploadSmallFile(B2UploadFileRequest + .builder(bucketId, name, B2ContentTypes.TEXT_PLAIN, B2ByteArrayContentSource.build(payload)) + .build()); + + step = "download"; + final B2ContentMemoryWriter sink = B2ContentMemoryWriter.build(); + client.downloadById(uploaded.getFileId(), sink); + if (!Arrays.equals(sink.getBytes(), payload)) { + fail("downloaded bytes differ from the uploaded bytes"); + } + + step = "list"; + boolean listed = false; + for (B2FileVersion version : client.fileNames(B2ListFileNamesRequest.builder(bucketId).setPrefix(name).build())) { + listed |= name.equals(version.getFileName()) && uploaded.getFileId().equals(version.getFileId()); + } + if (!listed) { + fail("the uploaded file is not listed"); + } + + step = "delete"; + client.deleteFileVersion(uploaded); + + step = "absence"; + for (B2FileVersion version : client.fileVersions(B2ListFileVersionsRequest.builder(bucketId).setPrefix(name).build())) { + if (name.equals(version.getFileName())) { + fail("a version is still listed after delete"); + } + } + } catch (Exception e) { + fail(e.getClass().getSimpleName() + " " + e.getMessage()); + } + System.out.println("GOLDEN-PATH PASS"); + } + + private static void fail(String detail) { + System.out.println("GOLDEN-PATH FAIL " + step + ": " + String.valueOf(detail).replaceAll("\\s+", " ")); + System.exit(1); + } +} diff --git a/.sdkharness/tests/run-health b/.sdkharness/tests/run-health new file mode 100755 index 000000000..55d4273f1 --- /dev/null +++ b/.sdkharness/tests/run-health @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Copyright 2026, Backblaze Inc. All Rights Reserved. +# License https://www.backblaze.com/using_b2_code.html +# +# sdkharness customer health (golden path) for this checkout. See ../README.md. +set -u + +LEVEL=health +SCENARIO=golden-path +HERE="$(cd "$(dirname "$0")" && pwd -P)" +result() { printf 'SDKHARNESS_RESULT\t%s\t%s\t%s\t%s\n' "$LEVEL" "$SCENARIO" "$1" "$2"; } +fail() { result FAIL "$1"; exit 1; } + +[ "${SDKHARNESS_TEST_LEVEL:-}" = "$LEVEL" ] && [ "${SDKHARNESS_SCENARIO:-}" = "$SCENARIO" ] \ + || fail 'configuration: unexpected contract identity' +[[ "${SDKHARNESS_SIMULATOR_URL:-}" =~ ^http://127\.0\.0\.1:[0-9]{1,5}/?$ ]] \ + || fail 'configuration: simulator URL must be an IPv4 loopback HTTP origin' +[ -n "${B2_BUCKET_NAME:-}" ] || fail 'configuration: B2_BUCKET_NAME is unset' +[ -n "${SDKHARNESS_JAVA_CLASSPATH:-}" ] \ + || fail 'configuration: SDKHARNESS_JAVA_CLASSPATH is unset (the core and httpclient jars built from this checkout, plus their runtime dependencies)' +command -v java >/dev/null 2>&1 || { result SKIP 'missing-runtime: no java on PATH'; exit 0; } + +# Only the simulator's fixed credential (in GoldenPath.java) reaches the SDK: every ambient B2_* +# is dropped. The bucket name is kept; it is a name, not a secret, and the harness creates it. +bucket="$B2_BUCKET_NAME" +for name in $(compgen -e); do case "$name" in B2_*) unset "$name" ;; esac; done +export B2_BUCKET_NAME="$bucket" + +log="$(mktemp "${TMPDIR:-/tmp}/sdkharness-java-health.XXXXXX")" || fail 'setup: could not create a log file' +trap 'rm -f "$log"' EXIT +if java -cp "$SDKHARNESS_JAVA_CLASSPATH" "$HERE/health/GoldenPath.java" >"$log" 2>&1; then rc=0; else rc=$?; fi +grep -v '^GOLDEN-PATH ' "$log" || true +verdicts="$(grep -c '^GOLDEN-PATH ' "$log" || true)" +[ "$verdicts" = 1 ] || fail "contract: expected one verdict, got $verdicts (exit $rc)" +verdict="$(grep '^GOLDEN-PATH ' "$log")" +case "$verdict" in + 'GOLDEN-PATH PASS') [ "$rc" = 0 ] || fail "contract: PASS exited $rc"; result PASS - ;; + 'GOLDEN-PATH FAIL '*) fail "${verdict#GOLDEN-PATH FAIL }" ;; + *) fail 'contract: malformed verdict' ;; +esac