test(consistency): exercise the Azure backend against Azurite

Adds an Azurite service and a scenario that audits the Azure backend
through `?storage=azurite`. It is the only coverage of that code path in
the tree: `AzureBlobBackend` has unit tests for its name parser and
ordering, but nothing else speaks the protocol, and a paid account is
not an option for CI. Azurite implements the real Blob REST API, so this
exercises SharedKey signing, prefix/marker paging, and the 256-way shard
walk with its termination.

## Harness

`docker-compose.test.yml` gains an azurite service on 10000 (tmpfs, so
it dies with the stack). `spawn-db.sh` provisions the container itself,
because `AzureBlobBackend::initialize` verifies rather than creates —
signed by hand with curl + openssl rather than pulling a ~700 MB `az`
image for one PUT. Two traps are commented there: the account key is
base64 but HMAC wants raw bytes, and the canonicalized resource repeats
the account name (`/{acc}/{acc}/{container}`) because the emulator puts
in the path what real Azure puts in the host. Getting that wrong yields
403, not a hint.

The `azurite` entry is declared in `server.env` but never activated, so
the suite's active backend stays local and only this file reaches Azure.

## What it asserts, and what it cannot

A failure surfaces as `ok: false`, because an enumeration error now
fails the run rather than degrading to a per-row probe.

It deliberately asserts no finding count. The container starts empty and
the job's grace window is an hour, so a freshly-uploaded blob is skipped
in both directions by design — an audit here can only report zero, and
"zero findings" would pass whether enumeration worked or returned
nothing. The one positive assert, `scanned_count != 0`, therefore sits
on the local control, which does hold blobs; `scanned_count` accumulates
via `checkpoint`, which the empty-page early return skips.

## No cutover, deliberately

Putting real bytes in the container means `backend_migration
?storage=azurite`, which hangs on the first blob: `head_check` issues a
~40-byte ranged GET, `azure_core` 0.21 attaches
`x-ms-range-get-content-crc64` to anything under 4 MiB, Azurite 500s,
and the deterministic error is retried forever while
`migration_readonly` refuses writes app-wide. The full chain and the
rejected workaround are in the file header. The scenario is still
ordered last in `run.sh` — it is the only one needing a second service,
and the cutover comes back there once the official SDK lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edouard Vanbelle
2026-09-02 19:24:46 +02:00
parent 2b52d233f0
commit 04e0df0c89
6 changed files with 404 additions and 4 deletions
+19 -1
View File
@@ -42,7 +42,7 @@ OXICLOUD_NEXTCLOUD_ENABLED=true
# `s3_stub` is declared but never activated — it lets storage_config.hurl
# assert the entries table has more than one row without needing a
# real S3 backend.
OXICLOUD_STORAGE_ENTRIES=local_main,s3_stub
OXICLOUD_STORAGE_ENTRIES=local_main,s3_stub,azurite
OXICLOUD_STORAGE_local_main_BACKEND=local
OXICLOUD_STORAGE_s3_stub_BACKEND=s3
OXICLOUD_STORAGE_s3_stub_S3_BUCKET=oxicloud-test-stub
@@ -51,6 +51,24 @@ OXICLOUD_STORAGE_s3_stub_S3_ENDPOINT_URL=http://127.0.0.1:9999
OXICLOUD_STORAGE_s3_stub_S3_ACCESS_KEY=stub
OXICLOUD_STORAGE_s3_stub_S3_SECRET_KEY=stub
# `azurite` — a REAL, reachable Azure backend, unlike `s3_stub` above.
# It points at the Azurite emulator started by spawn-db.sh, which speaks
# the actual Blob REST API, so this is the only way to exercise the Azure
# path without an account.
#
# Declared but NOT activated: the active backend stays local_main, so the
# rest of the suite is untouched. Tests reach it explicitly through
# `?storage=azurite` on the jobs that accept an entry name — which is
# exactly what that parameter was added for.
#
# The credentials are Azurite's fixed, publicly-documented development
# pair. They are in every Microsoft quickstart; nothing here is secret.
OXICLOUD_STORAGE_azurite_BACKEND=azure
OXICLOUD_STORAGE_azurite_AZURE_ACCOUNT_NAME=devstoreaccount1
OXICLOUD_STORAGE_azurite_AZURE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
OXICLOUD_STORAGE_azurite_AZURE_CONTAINER=oxicloud-test
OXICLOUD_STORAGE_azurite_AZURE_ENDPOINT_URL=http://127.0.0.1:10000/devstoreaccount1
RUST_LOG="warn,audit=info,sqlx::migrate=info"
#RUST_LOG="warn,audit=info,oxicloud::quota=debug"
#RUST_LOG=debug