Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .sdkharness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,13 @@ Executables print one five-field, tab-separated result:
SDKHARNESS_RESULT health golden-path PASS -
```

The customer-health executable accepts only a literal loopback HTTP simulator
URL. It never uses a production B2 endpoint or real credentials.
The customer-health, conformance, and resilience executables accept only
literal IPv4 loopback HTTP simulator URLs. They import `b2sdk` from this exact
checkout and never use a production B2 endpoint or real credentials.

Conformance owns 33 capability checks and resilience owns 16 injected-fault
checks. Their small dispatchers validate the invocation, run the selected
repository-owned assertion, and translate its standing verdict into the
five-field `SDKHARNESS_RESULT` record. The central harness continues to own
scenario selection, simulator lifecycle, fleet evidence, issue reconciliation,
reporting, and notification.
49 changes: 49 additions & 0 deletions .sdkharness/tests.tsv
Original file line number Diff line number Diff line change
@@ -1,2 +1,51 @@
test_level scenario target executable
health golden-path simulator ./.sdkharness/tests/health-golden-path
conformance bucket.cors simulator ./.sdkharness/tests/run-conformance
conformance bucket.crud simulator ./.sdkharness/tests/run-conformance
conformance bucket.lifecycle simulator ./.sdkharness/tests/run-conformance
conformance bucket.notification_rules simulator ./.sdkharness/tests/run-conformance
conformance bucket.replication_config simulator ./.sdkharness/tests/run-conformance
conformance bucket.replication_helper simulator ./.sdkharness/tests/run-conformance
conformance client.auth_persistence simulator ./.sdkharness/tests/run-conformance
conformance client.progress simulator ./.sdkharness/tests/run-conformance
conformance client.simulator simulator ./.sdkharness/tests/run-conformance
conformance client.sync simulator ./.sdkharness/tests/run-conformance
conformance enc.sse_b2 simulator ./.sdkharness/tests/run-conformance
conformance enc.sse_c simulator ./.sdkharness/tests/run-conformance
conformance files.delete_version simulator ./.sdkharness/tests/run-conformance
conformance files.download_by_id simulator ./.sdkharness/tests/run-conformance
conformance files.download_content simulator ./.sdkharness/tests/run-conformance
conformance files.hide simulator ./.sdkharness/tests/run-conformance
conformance files.list simulator ./.sdkharness/tests/run-conformance
conformance files.metadata simulator ./.sdkharness/tests/run-conformance
conformance files.server_side_copy simulator ./.sdkharness/tests/run-conformance
conformance files.upload simulator ./.sdkharness/tests/run-conformance
conformance keys.crud simulator ./.sdkharness/tests/run-conformance
conformance keys.multi_bucket simulator ./.sdkharness/tests/run-conformance
conformance large.compose_mixed simulator ./.sdkharness/tests/run-conformance
conformance large.concurrent_parts simulator ./.sdkharness/tests/run-conformance
conformance large.multipart simulator ./.sdkharness/tests/run-conformance
conformance large.parallel_download simulator ./.sdkharness/tests/run-conformance
conformance large.resume simulator ./.sdkharness/tests/run-conformance
conformance large.unbound_incremental simulator ./.sdkharness/tests/run-conformance
conformance lock.bucket_default simulator ./.sdkharness/tests/run-conformance
conformance lock.bypass_governance simulator ./.sdkharness/tests/run-conformance
conformance lock.legal_hold simulator ./.sdkharness/tests/run-conformance
conformance lock.per_file_retention simulator ./.sdkharness/tests/run-conformance
conformance urls.native_download simulator ./.sdkharness/tests/run-conformance
resilience api.backoff_503 simulator ./.sdkharness/tests/run-resilience
resilience api.retry_after_429 simulator ./.sdkharness/tests/run-resilience
resilience api.retry_after_503 simulator ./.sdkharness/tests/run-resilience
resilience auth.clock_expiry simulator ./.sdkharness/tests/run-resilience
resilience auth.expired_401 simulator ./.sdkharness/tests/run-resilience
resilience download.retry_503 simulator ./.sdkharness/tests/run-resilience
resilience part.retry_503 simulator ./.sdkharness/tests/run-resilience
resilience upload.cap_exceeded_403 simulator ./.sdkharness/tests/run-resilience
resilience upload.expired_token_401 simulator ./.sdkharness/tests/run-resilience
resilience upload.get_url_503 simulator ./.sdkharness/tests/run-resilience
resilience upload.reset_before_response simulator ./.sdkharness/tests/run-resilience
resilience upload.reset_mid_request simulator ./.sdkharness/tests/run-resilience
resilience upload.retry_408 simulator ./.sdkharness/tests/run-resilience
resilience upload.retry_500 simulator ./.sdkharness/tests/run-resilience
resilience upload.retry_503 simulator ./.sdkharness/tests/run-resilience
resilience upload.stall simulator ./.sdkharness/tests/run-resilience
319 changes: 319 additions & 0 deletions .sdkharness/tests/conformance/bucket.cors
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
#!/usr/bin/env python3
"""b2-sdk-python x bucket.cors. PARTIAL CELL -- the boundary is asserted.

Scenario (approved 2026-09-22, one per capability, identical on every tool that
claims it):

Action set a CORS rule set, then read it back
Assert field-for-field equality

The card answers `partial` for this cell and states the boundary: CORS
round-trips as an UNTYPED dict with no typed model and no dedicated
accessor, unlike lifecycle and notification rules. That boundary is the
most falsifiable claim in the matrix, so it is asserted here too -- a typed
CorsRule or a get/set_cors_rules accessor appearing upstream FAILS this
check, because the card would then be stale.

EXPECTED VALUES -- B2's published contract, reviewed 2026-09-23.
Every field sent here is a documented rule field (corsRuleName, allowedOrigins,
allowedOperations, allowedHeaders, exposeHeaders, maxAgeSeconds), and
b2_download_file_by_name / b2_download_file_by_id are documented
allowedOperations values. Rules can be "set when you create the bucket with
b2_create_bucket, or updated on an existing bucket using b2_update_bucket", and
b2_list_buckets returns corsRules.
https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules
https://www.backblaze.com/apidocs/b2-update-bucket
https://www.backblaze.com/apidocs/b2-list-buckets
The `partial` boundary leg tests the card's claim about b2sdk's own surface;
B2's docs do not speak to it.

Written from bin/conformance/b2-sdk-python/files.upload, the reference check,
and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository
checkout; the API was taken from
docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises.
"""

import os
import sys
import uuid

SLUG = 'b2-sdk-python'
CAPABILITY = 'bucket.cors'
RULE = {
'corsRuleName': 'sdkharness-conf',
'allowedOrigins': ['https://example.com'],
'allowedOperations': ['b2_download_file_by_name'],
'allowedHeaders': ['range'],
'exposeHeaders': ['x-bz-content-sha1'],
'maxAgeSeconds': 3600,
}

# The closed reason set, mirrored from bin/conformance/README.md. The runner
# enforces it; naming one outside this tuple is a FAIL it manufactures.
REASONS = (
'no-realm-option',
'unreachable',
'unauthorized',
'no-credential',
'missing-runtime',
'not-claimed',
)

# A requested target maps to a realm or to amber. `simulator` is the harness's
# own server (bin/simulator/serve.mjs), whose URL the runner exports as
# CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option.
REALMS = {
'staging': 'staging',
'production': 'production',
'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None,
}

# The simulator's fixed test credential, which the runner also injects. At
# @simulator the check uses it directly, so a real B2_* value can never be
# sent there.
SIM_CREDENTIAL = ('test-key-id', 'test-key')


def credential(realm):
"""The (key id, key) pair for a realm: the fixed one at @simulator."""
if realm == REALMS['simulator']:
return SIM_CREDENTIAL
return os.environ.get('B2_APPLICATION_KEY_ID'), os.environ.get('B2_APPLICATION_KEY')


class Amber(Exception):
"""The question could not be asked. Reason must be in REASONS."""

def __init__(self, reason: str, detail: str) -> None:
assert reason in REASONS, reason
self.reason = reason
self.detail = detail


class Failure(Exception):
"""A golden-path step that did not do what the card claims."""

def __init__(self, step: str, detail: str) -> None:
self.step = step
self.detail = detail


def say(target: str, verdict: str) -> None:
print(f'CONFORMANCE {SLUG} {CAPABILITY} @{target}: {verdict}')


def note(text: str) -> None:
"""Context for the log. NOT a result line.

The runner reads only the CONFORMANCE line and a PASS carries no detail, so
anything qualifying the STRENGTH of the evidence is said here, where it
survives in the retained log without being mistaken for a verdict.
"""
print(f'note {SLUG} {CAPABILITY}: {text}')


def step(name, action):
"""Run one golden-path step, converting the environment's own errors.

A credential NEVER reaches the detail: only the exception class name and,
for a classified network/auth error, the class that classified it.
"""
try:
return action()
except (Amber, Failure):
raise
except Exception as error: # noqa: BLE001 -- classified below, never echoed
name_of = type(error).__name__
if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'):
raise Amber('unauthorized', f"{name_of} at step '{name}'") from error
if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'):
raise Amber('unreachable', f"{name_of} at step '{name}'") from error
raise Failure(name, name_of) from error


def refused(name, action, expected):
"""Assert something is REFUSED, and record what refused it.

`expected` is a tuple of exception class names. A different class -- or no
exception at all -- is a Failure, because the refusal IS the claim here.
"""
try:
action()
except (Amber, Failure):
raise
except Exception as error: # noqa: BLE001 -- classified, never echoed
name_of = type(error).__name__
if name_of in expected:
return name_of
raise Failure(name, f"refused with {name_of}, expected {'/'.join(expected)}") from error
raise Failure(name, 'the call was permitted; the claim under test is that it is refused')


def payload_of(size: int) -> bytes:
"""Deterministic filler of an exact size."""
unit = b'sdkharness conformance '
return (unit * (size // len(unit) + 1))[:size]


def prepare(target):
"""Realm, credentials and the INSTALLED package -- before any network call."""
realm = REALMS.get(target)
if realm is None:
detail = (
'CONFORMANCE_SIMULATOR_URL is unset'
if target == 'simulator'
else f"b2sdk exposes no '{target}' realm"
)
raise Amber('no-realm-option', detail)

if not all(credential(realm)):
raise Amber('no-credential', 'B2_APPLICATION_KEY_ID / B2_APPLICATION_KEY unset')

try:
import b2sdk.v3 as b2
except ImportError as error:
raise Amber('missing-runtime', 'b2sdk is not installed') from error

return b2, realm


def authorize(api, realm):
"""Authorize against the named realm. Credentials never leave this call."""
step(
'authenticate',
lambda: api.authorize_account(*credential(realm), realm=realm),
)
return api


def connect(target, **api_kwargs):
"""The common case: one authorized B2Api against the requested realm."""
b2, realm = prepare(target)
return b2, authorize(b2.B2Api(b2.InMemoryAccountInfo(), **api_kwargs), realm)


def new_bucket(api, **kwargs):
"""An ephemeral bucket, named as the contract requires."""
name = f'sdkharness-conf-{uuid.uuid4().hex[:12]}'
return step('create bucket', lambda: api.create_bucket(name, 'allPrivate', **kwargs))


def _delete_hard(api, bucket, version):
"""Delete one version, lifting a legal hold or a governance lock if it blocks."""
for attempt in (0, 1):
try:
bucket.delete_file_version(
version.id_, version.file_name, bypass_governance=attempt > 0
)
return
except Exception: # noqa: BLE001 -- cleanup must never mask the verdict
if attempt == 0:
try:
import b2sdk.v3 as b2

api.update_file_legal_hold(version.id_, version.file_name, b2.LegalHold.OFF)
except Exception: # noqa: BLE001
pass


def teardown(api, *buckets):
"""Owns what it created and destroys it, on the error path too."""
for bucket in buckets:
if bucket is None:
continue
try:
for unfinished in bucket.list_unfinished_large_files():
try:
bucket.cancel_large_file(unfinished.file_id)
except Exception: # noqa: BLE001
pass
except Exception: # noqa: BLE001
pass
try:
versions = [v for v, _folder in bucket.ls('', recursive=True, latest_only=False)]
except Exception: # noqa: BLE001
versions = []
for version in versions:
_delete_hard(api, bucket, version)
try:
api.delete_bucket(bucket)
except Exception: # noqa: BLE001
pass


def drop_keys(api, key_ids):
"""Application keys are resources too."""
for key_id in key_ids:
if not key_id:
continue
try:
api.delete_key_by_id(key_id)
except Exception: # noqa: BLE001
pass


def run(target: str) -> None:
b2, api = connect(target)

bucket = new_bucket(api)
try:
step('set the CORS rules', lambda: bucket.update(cors_rules=[RULE]))

fresh = step('read the bucket back', lambda: api.list_buckets(bucket_name=bucket.name))
if not fresh:
raise Failure('read back', 'the bucket disappeared from the listing')
read_back = fresh[0].cors_rules

if not isinstance(read_back, list) or len(read_back) != 1:
raise Failure('read back', f'the bucket reports {read_back!r} instead of one CORS rule')
rule = read_back[0]
if not isinstance(rule, dict):
raise Failure(
'read back', f'the CORS rule came back as {type(rule).__name__}, not a dict'
)
for key, expected in RULE.items():
if rule.get(key) != expected:
raise Failure('read back', f'CORS field {key} did not round-trip')

# --- the `partial` boundary ------------------------------------------
if hasattr(b2, 'CorsRule'):
raise Failure(
'boundary',
"b2sdk.v3 now exports a typed CorsRule; the card's `partial` answer is stale",
)
for accessor in ('get_cors_rules', 'set_cors_rules'):
if hasattr(b2.Bucket, accessor):
raise Failure(
'boundary',
f"Bucket.{accessor} now exists; the card's `partial` answer is stale",
)
if type(rule) is not dict:
raise Failure(
'boundary',
f'the CORS rule is a {type(rule).__name__}, not the untyped dict the card records',
)

note('boundary held: CORS round-trips as a plain dict, with no typed model and no accessor')
finally:
teardown(api, bucket)


def main() -> int:
target = os.environ.get('CONFORMANCE_TARGET', 'staging')
try:
run(target)
except Amber as amber:
say(target, f'COULD-NOT-RUN ({amber.reason} -- {amber.detail})')
return 0
except Failure as failure:
say(target, f'FAIL ({failure.step} -- {failure.detail})')
return 1
except Exception as error: # noqa: BLE001 -- never a silent crash
say(target, f'FAIL (setup -- {type(error).__name__})')
return 1
say(target, 'PASS')
return 0


if __name__ == '__main__':
sys.exit(main())
Loading
Loading