Skip to content

test(b2): tighten API-facing b2 test assertions - #240

Merged
ffumero2003 merged 10 commits into
mainfrom
test-issue-204-b2-api-assertions
Oct 30, 2026
Merged

ffumero2003 merged 10 commits into
mainfrom
test-issue-204-b2-api-assertions

Conversation

@ffumero2003

Copy link
Copy Markdown
Contributor

Context

Part of the test-integrity audit (#97). #204 is a checklist of low-risk items: tests that check less than their names say, plus request and response edges a single-line change could break with no test failing.

This PR covers the API-facing part of the ### b2 section, 21 items: compose, bucket attrs, iteration, delete, download, replication, webhooks, SSE-C, CORS update and the harness contract. Where the other sections are:

Section PR
base #232
b2/sync, x/..., internal/... #233
b2 core items: b2_test.go, integration_test.go, account info, writer, reader, progress (including TestObjectDeleteBypassGovernanceOption) a separate slice stacked on #225, which edits the same files

Changes

Test changes, plus one behavior-identical seam in production code.

The one non-test change. b2/compose.go gains an unexported validateComposePlatformSize(size, intLimit). The existing check calls it with the same native limit as before (uint64(^uint(0)>>1)), so behavior is unchanged. The test calls it with math.MaxInt32, so the 32-bit limit can be tested on a 64-bit host. #204 asks for this ("test the limit through a seam"), and the CI matrix only cross-builds for 32-bit, so this is the only way to test it.

Compose (compose_fuzz_test.go, compose_extended_wire_test.go)

  • FuzzComposeRange also rejects short, reversed and oversized canonical ranges.
  • TestComposeUploadedPartPlatformSize checks the explicit 32-bit boundaries and that the native call matches.
  • New tests for the mutation items:
    • zero-size uploaded part rejected;
    • single-byte range bytes=5-5 accepted;
    • reversed range rejected;
    • copied-size totals at the 10 TB cap;
    • exactly 10,000 parts accepted.
  • TestComposeCopyPartReauthAndBackoff asserts the wait (at least 1s) for 408, 429, 503 and 504, not just "retried once".

Bucket, iteration and delete

  • TestBucketAttrsCORSAndFileLock and TestBucketAttrsDefaultRetention decode an independent HTTP fixture and assert every field, instead of a hand-built base.Bucket.
  • Empty-page iteration:
    • many empty pages: bounded stack depth and exactly 5001 calls;
    • a page that does not advance: covers names, versions and unfinished lists, and the changing-ID half of the check.
  • The delete-bypass wire test also asserts fileId and fileName.
  • TestBucketDownloadFileByIDReachesTheWire is removed. It was a strict duplicate of TestDownloadFileByIDPublicRoundTripWire, which stays.

Replication, webhooks, SSE-C, CORS, harness

  • The three replication error rows assert the specific error, not just err != nil.
  • Webhooks:
    • the large-body signature test drops its grep of webhook.go source and keeps the behavioral checks;
    • the event-type test uses literal JSON instead of marshaling the same struct.
  • TestSDKHarnessContract parses each scenario's entry point and failure assertions, and a new test rejects a no-op scenario.
  • SSE-C:
    • the changelog text check is renamed TestDocsSSECReaderMethodTypeChangeDocumented, as a docs gate;
    • the missing/wrong-key test, which exercised the fake server's own key check, is replaced by TestSSECReadSendsRequestedKeyAndPropagatesRejection and TestSSECReadRejectsExplicitNilKeyBeforeRequest.
  • Both CORS update tests also assert allowedOrigins, allowedOperations and maxAgeSeconds.

Verification

Each compose break was applied on its own, as a flip and, for a guard, as a deletion, and run against the whole module:

Break Result
uploaded size <= 0 → < 0 validate zero-size part = <nil>, want positive-size error
range end < start → <= parseComposeRange = (0, 0, …"bytes=5-5"), want (5, 5, nil)
range-order guard deleted (6, 5, <nil>), want reversed-range rejection
knownSize arithmetic flipped knownSize = 5000000000199, true; want 4999999999999, true
part count > 10000 → >= Compose supports at most 10000 parts, want scripted error after successful validation
the seam's own boundary > → >= 32-bit validation of 2147483647 = … exceeds…, want nil
copied minimum (flip and deletion), part-count guard deletion already covered by TestMappingComposeCopyMinimumPart / TestComposeRejectsMoreThan10000PartsBeforeStarting

Every test passes once each break is restored.

  • go test -race -count=5 -shuffle=on ./b2/... passes with B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY, B2_ACCOUNT_ID and B2_ACCOUNT_KEY unset.
  • make ci passes every step except the local changelog check, which cannot see PR labels. This PR is skip-changelog, because the seam does not change behavior.
  • SDK panel review (deep, all five roles): no findings on this slice's items. Its one flagged item, TestObjectDeleteBypassGovernanceOption, is in b2/b2_test.go and belongs to the core slice. It reached this review through a scope-file mistake.
  • Live B2 and simulator scenarios: not run. The harness contract check is structural.

Part of #204

Cover zero-size uploads, inclusive and reversed ranges, copied total sizes around 10 TB, and acceptance of exactly 10000 parts. Assert waits for recoverable copy failures.

The approved unexported platform-limit helper is the only non-test change in this slice. The production call passes the same native int limit as before; tests exercise math.MaxInt32 directly without mutable global state.

Part of #204
Decode independent bucket response fixtures, assert every retained CORS field and delete identifier, and cover cursor stalls across all listing modes with bounded stack depth. Remove the redundant download-by-ID wire test; its fuller round-trip twin remains.

Part of #204
Assert specific replication validation errors. Check SSE-C headers and propagation of server rejections separately from SDK rejection of a nil key, and label the method-type changelog check as a documentation gate.

Part of #204
Use literal event JSON and retain signature behavior tests without grepping production source. Require harness scenarios to contain called Go entry points and failure assertions, with negative fixtures for no-op programs.

Part of #204
@ffumero2003 ffumero2003 added this to the 0.10.0 milestone Oct 29, 2026
@ffumero2003 ffumero2003 added the skip-changelog No changelog entry needed; skips the changelog check label Oct 29, 2026
@ffumero2003
ffumero2003 requested a review from goanpeca as a code owner October 29, 2026 22:30
@ffumero2003 ffumero2003 self-assigned this Oct 29, 2026
@ffumero2003

Copy link
Copy Markdown
Contributor Author

A deeper SDK panel audit of this PR raised two findings and three nits. 10df302 addresses them:

  • Fixed: a harness check that could not fail. The structural check in b2/sdkharness_contract_test.go confirmed that a scenario's main calls its entry point, but not that it uses the result. It now requires the result to go through a failure-reporting check. TestSDKHarnessRejectsNoOpScenario gains a fixture whose main calls runCheck() and discards the error, and the check now rejects it. With the new requirement removed, that fixture is accepted (red); restored, it passes.
  • Fixed (nit): 429 Retry-After was not pinned. The 429 row now sends Retry-After: 2 and requires a wait of at least 2s. With blazer's backoff made to ignore Retry-After, it fails with copy_part status 429 retry waited 1.02s, want at least 2s.
  • Out of scope here: TestObjectDeleteBypassGovernanceOption. That item is in b2/b2_test.go, which belongs to test: tighten minor test assertions (checklist) #204's core slice (the branch stacked on test: replace self-referential expected values in five tests #225). It is handled there, not in this PR.
  • Answered (nit): "the seam isn't exercised through production". On a 64-bit host, production can never reach the platform check, because the 5 GB part limit rejects a part long before the native int limit. That is why the seam exists. The test pins the 32-bit boundary through validateComposePlatformSize, and the production call site passes the same native limit as before.

go test -race -count=5 -shuffle=on ./b2/... passes with the B2 credentials unset. The branch is synced with main.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The contract test rejects valid CRLF scenarios and accepts guards that ignore the entry point’s error result, allowing scenarios that silently succeed despite errors to pass.

2 open issues
What changed in this PR

Tightens API-facing b2 tests as part of #204’s test-integrity audit, replacing weak assertions with independent fixtures and boundary checks.

Changes:

  • Adds compose boundary and retry-wait checks, with a behavior-preserving platform-size validation helper.
  • Strengthens bucket, pagination, delete, replication, webhook, SSE-C and CORS assertions.
  • Adds structural harness checks and removes a duplicate download test.

Verification here was limited to source inspection; no tests, live B2 checks or simulator scenarios were run. A human code owner should review the SSE-C credential-handling test changes.

File Description
b2/​webhook_test.go Uses literal event JSON and removes source-text checks.
b2/​update_cors_name_test.go Checks additional CORS fields using the wire-test helper.
b2/​ssec_wire_test.go Checks key headers, rejection propagation and nil-key handling.
b2/​ssec_test.go Labels the changelog assertion as a documentation gate.
b2/​sdkharness_contract_test.go Inspects scenario entry points and failure handling.
b2/​replication_setup_test.go Asserts specific replication errors.
b2/​iterator_advance_test.go Checks stalled cursors, stack depth and request counts.
b2/​downloadbyid_wire_test.go Removes a duplicate download-by-ID wire test.
b2/​delete_bypass_wire_test.go Checks file ID and name in delete requests.
b2/​default_retention_test.go Uses an independent HTTP response fixture.
b2/​create_cors_filelock_test.go Tests bucket attribute mapping from literal response JSON.
b2/​compose.go Extracts platform-size validation without changing behavior.
b2/​compose_fuzz_test.go Adds range, size and total-boundary assertions.
b2/​compose_extended_wire_test.go Checks retry delays and acceptance of exactly 10,000 parts.

Review effort: Balanced

Comment thread b2/sdkharness_contract_test.go Resolved
Comment thread b2/sdkharness_contract_test.go Resolved

@david-santacruz-backblaze david-santacruz-backblaze left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's review first the copilot flagged issues.

@ffumero2003

Copy link
Copy Markdown
Contributor Author

@david-santacruz-backblaze Both Copilot findings are fixed in a5922b9, and the branch is synced with main:

  • CRLF scenarios (the Windows CI failure): scenarios are normalized to LF before parsing, and a CRLF case pins it.
  • Unchecked initializer results: an if err := runCheck(); <cond> initializer now counts only when <cond> references err; the ; false example is a rejection row.

Each break was applied alone and fails its test. go test -race -count=5 -shuffle=on ./b2/..., go vet ./... and GOOS=windows go vet ./b2/... are clean. Ready for another look.

@david-santacruz-backblaze david-santacruz-backblaze left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two items before merge (Part of #204):

  1. b2/compose_fuzz_test.go: the nonfinal minimum check (end-start < minPartSize-1, b2/compose.go) is only tested with small offsets (n = len(value)), so end+start would still pass. Add to an existing table test: bytes=9000000-9000010 → rejected, bytes=9000000-14999999 → accepted. Parse-only, no runtime cost. Prove it by flipping the check to end+start and confirming the new row fails.
  2. b2/downloadbyid_wire_test.go is deleted. Confirm TestDownloadFileByIDPublicRoundTripWire asserts method GET, the fileId query parameter and the Authorization header. If it doesn't, add those assertions there.

@ffumero2003

Copy link
Copy Markdown
Contributor Author

@david-santacruz-backblaze Thanks, both points:

  1. Nonfinal minimum with large offsets: fixed in c955a6e. The existing FuzzComposeRange table now has literal large-offset rows: bytes=9000000-9000010 nonfinal → rejected (11 bytes); bytes=9000000-13999999 → accepted (exactly 5,000,000 bytes); bytes=9000000-14999999 → accepted. Flipping the check in b2/compose.go to end+start fails with validate("bytes=9000000-9000010", nonFinal=true) error = <nil>, want error=true, and changing minPartSize-1 to minPartSize fails the exact-5 MB row.
  2. Download-by-ID coverage: already in place. downloadbyid_wire_test.go was removed because test: tighten minor test assertions (checklist) #204 listed it as a strict duplicate. The remaining TestDownloadFileByIDPublicRoundTripWire asserts the method, the fileId query and the account token in one check at b2/downloadbyid_extended_wire_test.go:185-186: request.method != http.MethodGet || request.path != "/b2api/v4/b2_download_file_by_id" || request.query != "fileId=file-id" || request.headers.Get("Authorization") != "t".

The branch is synced with main; go test -race -count=3 -shuffle=on ./b2/... and go vet ./... are clean.

@ffumero2003
ffumero2003 merged commit 34c2bbb into main Oct 30, 2026
23 of 24 checks passed
@ffumero2003
ffumero2003 deleted the test-issue-204-b2-api-assertions branch October 30, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog No changelog entry needed; skips the changelog check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants