From 6b47141ef10881ecc8194174630c802c82b419a6 Mon Sep 17 00:00:00 2001 From: scarreras Date: Thu, 8 Oct 2026 22:40:42 -0300 Subject: [PATCH] Add the sdkharness customer health check (golden path) Adds a versioned .sdkharness/ contract with one scenario, health/golden-path: a single-file Java program that authorizes, uploads, downloads and compares the bytes, lists, deletes and confirms absence against the harness's loopback simulator. The harness builds the core and httpclient jars from this checkout and passes them as SDKHARNESS_JAVA_CLASSPATH; the build is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- .sdkharness/README.md | 40 +++++++++ .sdkharness/tests.tsv | 2 + .sdkharness/tests/health/GoldenPath.java | 101 +++++++++++++++++++++++ .sdkharness/tests/run-health | 40 +++++++++ 4 files changed, 183 insertions(+) create mode 100644 .sdkharness/README.md create mode 100644 .sdkharness/tests.tsv create mode 100644 .sdkharness/tests/health/GoldenPath.java create mode 100755 .sdkharness/tests/run-health 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