| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| FAIL | upload blob for BUD-01 tests HTTP 400 | Body: missing auth event | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Headers access-control-allow-headers: (missing) | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Methods access-control-allow-methods: (missing) | |
| PASS | error response has 4xx/5xx status | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 400 | Body: missing auth event | |
| FAIL | upload endpoint is accessible at /upload HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| PASS | upload without X-SHA-256 returns 200/201 or 400 if required | |
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 401 | Body: invalid x tag | |
| WARN | descriptor contains required fields — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | descriptor sha256 matches — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | descriptor size matches — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | descriptor type reflects MIME — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | descriptor uploaded is timestamp — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | descriptor url includes extension — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | new blob returns 201 Created HTTP 400 | Body: missing auth event | |
| WARN | duplicate blob — first upload succeeds HTTP 400 | Body: missing auth event | |
| PASS | duplicate blob returns 200 OK | |
| WARN | server does not modify blob — upload succeeds HTTP 400 | Body: missing auth event | |
| WARN | X-SHA-256 header is accepted HTTP 400 | Body: missing auth event | |
| WARN | upload works without prior HEAD /upload request HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-04 tests HTTP 400 | Body: missing auth event | |
| PASS | malformed request body returns 400 | |
| PASS | missing url in body returns 400 | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 400 | Body: download error |
| Test | ||
|---|---|---|
| WARN | PUT /media accepts binary data and returns 200 or 201 HTTP 400 | Body: missing auth event | |
| WARN | PUT /media returns blob descriptor on success HTTP 400 | Body: missing auth event | |
| WARN | PUT /media X-SHA-256 header is accepted HTTP 400 | Body: missing auth event | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 401 | Body: invalid action in auth event | |
| WARN | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers HTTP 401 | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 401 | |
| PASS | HEAD /media X-Reason header on error — status is 4xx |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 401 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 401 | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 200 | |
| WARN | HEAD /upload X-Reason on error — status is 4xx HTTP 200 | |
| PASS | preflight 200 allows subsequent PUT upload | |
| WARN | preflight followed by PUT upload succeeds HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| WARN | 402 Payment Required — server does not require payment HTTP 400 | Body: missing auth event | |
| WARN | X-Cashu — server does not require payment HTTP 400 | Body: missing auth event | |
| WARN | X-Lightning — server does not require payment HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-08 tests HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| WARN | GET /<sha256> with t=get auth — upload setup succeeds HTTP 400 | Body: missing auth event | |
| WARN | PUT /upload with t=upload auth succeeds HTTP 400 | Body: missing auth event | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /media with t=media auth succeeds HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-12 tests HTTP 400 | Body: missing auth event |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Headers access-control-allow-headers: (missing) | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Methods access-control-allow-methods: (missing) | |
| PASS | error response has 4xx/5xx status | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| PASS | Access-Control-Allow-Origin: * on GET | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | accept-ranges contains 'bytes' | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | 206 response has Content-Range header | |
| PASS | Content-Range contains 'bytes' | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 400 | X-Reason: unsupported content type: application/octet-stream | Body: unsupported content type: application/octet-stream | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 401 | X-Reason: obj hashed wrong once uploaded, got d79c664adbeaba10fa1fd1f31d25dbcc8862684ba9f1b7c718510b017a5fd211 | Body: obj hashed wrong once uploaded, got d79c664adbeaba10fa1fd1f31d25dbcc8862684ba9f1b7c718510b017a5fd211 | |
| PASS | descriptor contains required fields — upload succeeds | |
| PASS | descriptor is valid JSON with required fields | |
| PASS | descriptor has url field | |
| PASS | descriptor has sha256 field | |
| PASS | descriptor has size field | |
| PASS | descriptor has type field | |
| PASS AUTH | descriptor has uploaded field | |
| PASS | descriptor sha256 matches — upload succeeds | |
| PASS | descriptor sha256 matches uploaded blob hash | |
| PASS | descriptor size matches — upload succeeds | |
| PASS | descriptor size is greater than 0 | |
| PASS | descriptor type reflects MIME — upload succeeds | |
| PASS | descriptor has type field | |
| PASS | descriptor uploaded is timestamp — upload succeeds | |
| PASS | descriptor uploaded is an integer | |
| PASS | descriptor uploaded > 0 | |
| PASS | descriptor url includes extension — upload succeeds | |
| PASS | descriptor URL includes file extension | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 400 | X-Reason: unsupported content type: application/octet-stream | Body: unsupported content type: application/octet-stream | |
| WARN AUTH | new blob returns 201 Created HTTP 200 | Body: {"type":"image\/png","size":66289,"owner":"87b45e681cb8ce5e57fd2225bf0033396ade8468f76b9caec6421ee9d0670c23","sha256":"de3ad7a310929354c731302aeb2f7c3c4b21305f6105ebb630eb49b15423459e","uploaded":1777 | |
| PASS | duplicate blob — first upload succeeds | |
| WARN | duplicate blob returns 200 OK HTTP 400 | X-Reason: already got that file | Body: already got that file | |
| PASS | server does not modify blob — upload succeeds | |
| PASS | server does not modify blob — GET succeeds | |
| PASS | downloaded blob hash matches original | |
| PASS | X-SHA-256 header is accepted | |
| PASS AUTH | upload works without prior HEAD /upload request |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-04 tests | |
| WARN | mirror endpoint accepts URL in request body HTTP 400 | X-Reason: already got that file | Body: already got that file | |
| WARN | mirrored blob returns blob descriptor HTTP 400 | X-Reason: already got that file | Body: already got that file | |
| WARN | existing mirror returns 200 OK HTTP 400 | X-Reason: already got that file | Body: already got that file | |
| WARN | malformed request body returns 400 HTTP 401 | X-Reason: expect URL in json of body | Body: expect URL in json of body | |
| WARN | missing url in body returns 400 HTTP 401 | X-Reason: expect URL in json of body | Body: expect URL in json of body | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 400 | X-Reason: already failed: download side failed: HTTPSConnectionPool(host='nonexistent.invalid.domain.fake', port=443): Max retries exceeded with url: /cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc (Caused by ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 503 Service Unavailable'))) | Body: already failed: download side failed: HTTPSConnectionPool(host='nonexistent.invalid.domain.fake', port=443): Max retries exceeded with url: /ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| Test | ||
|---|---|---|
| PASS AUTH | PUT /media accepts binary data and returns 200 or 201 | |
| PASS | PUT /media returns blob descriptor on success | |
| PASS | media descriptor has url | |
| PASS | media descriptor has sha256 | |
| PASS | media descriptor has size | |
| PASS | media descriptor has type | |
| PASS | media descriptor has uploaded | |
| PASS | PUT /media X-SHA-256 header is accepted | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 401 | X-Reason: obj hashed wrong once uploaded, got 684474079fae8fe734033bb704a8dd89663f0f8d2ba9c8559a668754b9fe0c14 | Body: obj hashed wrong once uploaded, got 684474079fae8fe734033bb704a8dd89663f0f8d2ba9c8559a668754b9fe0c14 | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 400 | X-Reason: missing X-Content-Size header | |
| PASS | HEAD /media X-Reason header on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 400 | X-Reason: missing X-Content-Size header | |
| PASS | HEAD /upload missing X-SHA-256 returns 400 | |
| PASS | HEAD /upload malformed X-SHA-256 returns 400 | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 400 | X-Reason: size too big | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| PASS | preflight 200 allows subsequent PUT upload | |
| PASS AUTH | preflight followed by PUT upload succeeds |
| Test | ||
|---|---|---|
| PASS AUTH | 402 Payment Required — server does not require payment | |
| PASS | X-Cashu — server does not require payment | |
| PASS | X-Lightning — server does not require payment |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-08 tests | |
| WARN | mirror response nip94 test — mirror succeeds HTTP 400 | X-Reason: already got that file | Body: already got that file |
| Test | ||
|---|---|---|
| PASS | PUT /report accepts signed kind:1984 report event | |
| PASS | PUT /report with multiple x tags is accepted | |
| PASS | PUT /report with e/p tags is accepted | |
| WARN | PUT /report with invalid blob hash is rejected with 4xx/5xx HTTP 200 | Body: Thanks for this report, it is recorded for processing. |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| PASS AUTH | GET /<sha256> with t=get auth — upload setup succeeds | |
| PASS | GET /<sha256> with t=get auth returns 200 | |
| PASS AUTH | PUT /upload with t=upload auth succeeds | |
| PASS | DELETE /<sha256> with t=delete auth succeeds | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /mirror with t=upload auth succeeds HTTP 400 | X-Reason: already got that file | Body: already got that file | |
| PASS AUTH | PUT /media with t=media auth succeeds | |
| WARN | DELETE without authorization returns 401 HTTP 502 | Body: 502 error from 24242.io (level 0) | |
| PASS | DELETE with wrong x tag returns 401 or 403 |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-12 tests | |
| PASS | GET /list/<pubkey> returns 200 | |
| PASS | GET /list/<pubkey> returns JSON array | |
| PASS | blob descriptors contain required fields — list succeeds | |
| PASS | list has at least one entry | |
| PASS | list descriptor has url | |
| PASS | list descriptor has sha256 | |
| PASS | list descriptor has size | |
| PASS | list descriptor has type | |
| PASS | list descriptor has uploaded | |
| PASS | GET /list/<pubkey>?limit=1 returns 200 | |
| PASS | limit=1 returns at most 1 result | |
| PASS | cursor pagination — initial list succeeds | |
| PASS | initial list has entries | |
| PASS | cursor pagination — second page returns 200 | |
| WARN | cursor excludes previous entry | |
| PASS | results sorted by uploaded date descending — list succeeds | |
| WARN | GET /list returns 400 for malformed query params HTTP 200 | Body: [ {"sha256":"6db16858768b93df1082fd5a78529b9402e96be25a44dcea22928bc4154bdc94","size":67178,"owner":"270a6807a4c60a8b35b1401c12ee388dcae996c6441675aba0259f75c553ac8f","type":"image\/png","uploaded":1 | |
| PASS AUTH | delete test — upload setup succeeds | |
| PASS | delete returns 200 or 204 on success | |
| PASS | deleted blob returns 404 on GET | |
| PASS | delete non-existent blob returns 404 | |
| PASS | delete without authorization returns 401 | |
| PASS | delete requires x tag — upload succeeds | |
| PASS | delete with wrong x tag returns 401 or 403 | |
| PASS | multiple x tags — delete blob1 succeeds | |
| PASS | multiple x tags do not delete multiple blobs |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| FAIL | GET response includes Access-Control-Allow-Origin: * access-control-allow-origin: (missing) | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Headers | |
| PASS | OPTIONS Allow-Headers includes Authorization | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Methods | |
| PASS | OPTIONS Allow-Methods includes GET | |
| PASS | OPTIONS Allow-Methods includes HEAD | |
| PASS | OPTIONS Allow-Methods includes PUT | |
| PASS | OPTIONS Allow-Methods includes DELETE | |
| PASS | error response has 4xx/5xx status | |
| PASS | X-Reason header is non-empty when present | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| FAIL | Access-Control-Allow-Origin: * on GET access-control-allow-origin: (missing) | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | 206 response has Content-Range header | |
| PASS | Content-Range contains 'bytes' | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 415 | X-Reason: File type not allowed, unsupported. Please check https://nostr.build for supported file types and paid options | Body: File type not allowed, unsupported. Please check https://nostr.build for supported file types and paid options | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: Error uploading the file: File hash mismatch | Body: Error uploading the file: File hash mismatch | |
| PASS | descriptor contains required fields — upload succeeds | |
| PASS | descriptor is valid JSON with required fields | |
| PASS | descriptor has url field | |
| PASS | descriptor has sha256 field | |
| PASS | descriptor has size field | |
| PASS | descriptor has type field | |
| PASS AUTH | descriptor has uploaded field | |
| PASS | descriptor sha256 matches — upload succeeds | |
| PASS | descriptor sha256 matches uploaded blob hash | |
| PASS | descriptor size matches — upload succeeds | |
| PASS | descriptor size is greater than 0 | |
| PASS | descriptor type reflects MIME — upload succeeds | |
| PASS | descriptor has type field | |
| PASS | descriptor uploaded is timestamp — upload succeeds | |
| PASS | descriptor uploaded is an integer | |
| PASS | descriptor uploaded > 0 | |
| PASS | descriptor url includes extension — upload succeeds | |
| PASS | descriptor URL includes file extension | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 415 | X-Reason: File type not allowed, unsupported. Please check https://nostr.build for supported file types and paid options | Body: File type not allowed, unsupported. Please check https://nostr.build for supported file types and paid options | |
| WARN AUTH | new blob returns 201 Created HTTP 200 | Body: {"url":"https://npub14axdszarth5a929mjkpukztn5l6pz3np392wxpftzc6muhml79dscs29wm.blossom.band/c84e1bdabe36f4290a3966d21b611195b836b05a336d1572504cc5db2c3d96b5.png","size":68167,"type":"image/png","sha2 | |
| PASS | duplicate blob — first upload succeeds | |
| PASS | duplicate blob returns 200 OK | |
| PASS | server does not modify blob — upload succeeds | |
| PASS | server does not modify blob — GET succeeds | |
| PASS | downloaded blob hash matches original | |
| PASS | X-SHA-256 header is accepted | |
| PASS AUTH | upload works without prior HEAD /upload request |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-04 tests | |
| WARN | mirror endpoint accepts URL in request body HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL | |
| WARN | mirrored blob returns blob descriptor HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL | |
| WARN | existing mirror returns 200 OK HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL | |
| WARN | malformed request body returns 400 HTTP 500 | X-Reason: Internal Server Error | Body: Unexpected token 'o', "not json" is not valid JSON | |
| PASS | missing url in body returns 400 | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 400 | X-Reason: Error uploading the file: Access to the special-use domain 'invalid' is not allowed | Body: Error uploading the file: Access to the special-use domain 'invalid' is not allowed |
| Test | ||
|---|---|---|
| PASS AUTH | PUT /media accepts binary data and returns 200 or 201 | |
| PASS | PUT /media returns blob descriptor on success | |
| PASS | media descriptor has url | |
| PASS | media descriptor has sha256 | |
| PASS | media descriptor has size | |
| PASS | media descriptor has type | |
| PASS | media descriptor has uploaded | |
| PASS | PUT /media X-SHA-256 header is accepted | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: Error uploading the file: File hash mismatch | Body: Error uploading the file: File hash mismatch | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 400 | X-Reason: Missing one or more required header(s): x-content-length, x-content-type, x-sha-256 | |
| PASS | HEAD /media X-Reason header on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 400 | X-Reason: Missing one or more required header(s): x-content-length, x-content-type, x-sha-256 | |
| PASS | HEAD /upload missing X-SHA-256 returns 400 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 200 | |
| PASS | HEAD /upload too large X-Content-Length returns 413 | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| PASS | preflight 200 allows subsequent PUT upload | |
| PASS AUTH | preflight followed by PUT upload succeeds |
| Test | ||
|---|---|---|
| PASS AUTH | 402 Payment Required — server does not require payment | |
| PASS | X-Cashu — server does not require payment | |
| PASS | X-Lightning — server does not require payment |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-08 tests | |
| PASS | nip94 field is a JSON array when present | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 contains url tag | |
| WARN | nip94 url tag matches descriptor url | |
| PASS | nip94 contains x tag matching sha256 | |
| PASS | nip94 x tag matches sha256 | |
| PASS | nip94 contains m tag matching MIME type | |
| PASS | nip94 m tag matches type | |
| WARN | mirror response nip94 test — mirror succeeds HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL |
| Test | ||
|---|---|---|
| WARN | PUT /report accepts signed kind:1984 report event HTTP 404 | Body: Not Found /blossom.band/report | |
| WARN | PUT /report with multiple x tags is accepted HTTP 404 | Body: Not Found /blossom.band/report | |
| WARN | PUT /report with e/p tags is accepted HTTP 404 | Body: Not Found /blossom.band/report | |
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| PASS AUTH | GET /<sha256> with t=get auth — upload setup succeeds | |
| PASS | GET /<sha256> with t=get auth returns 200 | |
| PASS AUTH | PUT /upload with t=upload auth succeeds | |
| PASS | DELETE /<sha256> with t=delete auth succeeds | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /mirror with t=upload auth succeeds HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL | |
| PASS AUTH | PUT /media with t=media auth succeeds | |
| PASS | DELETE without authorization returns 401 | |
| WARN | DELETE with wrong x tag returns 401 or 403 HTTP 400 | X-Reason: Invalid x tag in Blossom Authorization event | Body: Invalid x tag in Blossom Authorization event |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-12 tests | |
| PASS | GET /list/<pubkey> returns 200 | |
| PASS | GET /list/<pubkey> returns JSON array | |
| PASS | blob descriptors contain required fields — list succeeds | |
| PASS | list has at least one entry | |
| PASS | list descriptor has url | |
| PASS | list descriptor has sha256 | |
| PASS | list descriptor has size | |
| PASS | list descriptor has type | |
| PASS | list descriptor has uploaded | |
| PASS | GET /list/<pubkey>?limit=1 returns 200 | |
| PASS | limit=1 returns at most 1 result | |
| PASS | cursor pagination — initial list succeeds | |
| PASS | initial list has entries | |
| PASS | cursor pagination — second page returns 200 | |
| WARN | cursor excludes previous entry | |
| PASS | results sorted by uploaded date descending — list succeeds | |
| WARN | GET /list returns 400 for malformed query params HTTP 200 | Body: [{"url":"https://npub1zqhlfs5xsd4zgnd23alwh5d2kwjj00wzfpvwlvz5dz488jdgfcms502nkp.blossom.band/5cb1d7c4a0b9e917da46df37bd06a2055c5133bc21cb90d81b904e46578159df.png","sha256":"5cb1d7c4a0b9e917da46df37bd | |
| WARN | delete test — upload setup succeeds HTTP 429 | Body: <!doctype html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
<!--[if IE 8]> <html class="no- | |
| PASS | delete returns 200 or 204 on success | |
| PASS | deleted blob returns 404 on GET | |
| WARN | delete non-existent blob returns 404 HTTP 204 | |
| PASS | delete without authorization returns 401 | |
| WARN | delete requires x tag — upload succeeds HTTP 429 | Body: <!doctype html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
<!--[if IE 8]> <html class="no- | |
| WARN | delete with wrong x tag returns 401 or 403 HTTP 400 | X-Reason: Invalid x tag in Blossom Authorization event | Body: Invalid x tag in Blossom Authorization event | |
| PASS | multiple x tags — delete blob1 succeeds | |
| WARN | multiple x tags do not delete multiple blobs HTTP 404 | Body: Not Found |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Headers access-control-allow-headers: (missing) | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Methods access-control-allow-methods: (missing) | |
| PASS | error response has 4xx/5xx status | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| PASS | Access-Control-Allow-Origin: * on GET | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | 206 response has Content-Range header | |
| PASS | Content-Range contains 'bytes' | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 401 | X-Reason: Server dose not accept application/octet-stream blobs | Body: Unauthorized | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request | |
| PASS | descriptor contains required fields — upload succeeds | |
| PASS | descriptor is valid JSON with required fields | |
| PASS | descriptor has url field | |
| PASS | descriptor has sha256 field | |
| PASS | descriptor has size field | |
| PASS | descriptor has type field | |
| PASS AUTH | descriptor has uploaded field | |
| PASS | descriptor sha256 matches — upload succeeds | |
| PASS | descriptor sha256 matches uploaded blob hash | |
| PASS | descriptor size matches — upload succeeds | |
| PASS | descriptor size is greater than 0 | |
| PASS | descriptor type reflects MIME — upload succeeds | |
| PASS | descriptor has type field | |
| PASS | descriptor uploaded is timestamp — upload succeeds | |
| PASS | descriptor uploaded is an integer | |
| PASS | descriptor uploaded > 0 | |
| PASS | descriptor url includes extension — upload succeeds | |
| PASS | descriptor URL includes file extension | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 401 | X-Reason: Server dose not accept application/octet-stream blobs | Body: Unauthorized | |
| WARN AUTH | new blob returns 201 Created HTTP 200 | Body: {"sha256":"d76583ebbbaca1518f42306f447f62cb0383a0ddb25c117f2662e59ea420949e","size":68973,"uploaded":1777471152,"type":"image/png","url":"https://blossom.yakihonne.com/d76583ebbbaca1518f42306f447f62cb | |
| PASS | duplicate blob — first upload succeeds | |
| PASS | duplicate blob returns 200 OK | |
| PASS | server does not modify blob — upload succeeds | |
| PASS | server does not modify blob — GET succeeds | |
| PASS | downloaded blob hash matches original | |
| PASS | X-SHA-256 header is accepted | |
| PASS AUTH | upload works without prior HEAD /upload request |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-04 tests | |
| WARN | mirror endpoint accepts URL in request body HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> | |
| WARN | mirrored blob returns blob descriptor HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> | |
| WARN | existing mirror returns 200 OK HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> | |
| WARN | malformed request body returns 400 HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> | |
| WARN | missing url in body returns 400 HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> |
| Test | ||
|---|---|---|
| PASS AUTH | PUT /media accepts binary data and returns 200 or 201 | |
| PASS | PUT /media returns blob descriptor on success | |
| PASS | media descriptor has url | |
| PASS | media descriptor has sha256 | |
| PASS | media descriptor has size | |
| PASS | media descriptor has type | |
| PASS | media descriptor has uploaded | |
| PASS | PUT /media X-SHA-256 header is accepted | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /media X-Reason header on error — status is 4xx HTTP 200 |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 200 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 200 | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 200 | |
| WARN | HEAD /upload X-Reason on error — status is 4xx HTTP 200 | |
| PASS | preflight 200 allows subsequent PUT upload | |
| PASS AUTH | preflight followed by PUT upload succeeds |
| Test | ||
|---|---|---|
| PASS AUTH | 402 Payment Required — server does not require payment | |
| PASS | X-Cashu — server does not require payment | |
| PASS | X-Lightning — server does not require payment |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-08 tests | |
| WARN | mirror response nip94 test — mirror succeeds HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> |
| Test | ||
|---|---|---|
| WARN | PUT /report accepts signed kind:1984 report event HTTP 405 | Body: Method Not Allowed | |
| WARN | PUT /report with multiple x tags is accepted HTTP 405 | Body: Method Not Allowed | |
| WARN | PUT /report with e/p tags is accepted HTTP 405 | Body: Method Not Allowed | |
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| PASS AUTH | GET /<sha256> with t=get auth — upload setup succeeds | |
| PASS | GET /<sha256> with t=get auth returns 200 | |
| PASS AUTH | PUT /upload with t=upload auth succeeds | |
| PASS | DELETE /<sha256> with t=delete auth succeeds | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /mirror with t=upload auth succeeds HTTP 403 | Body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html> | |
| PASS AUTH | PUT /media with t=media auth succeeds | |
| PASS | DELETE without authorization returns 401 | |
| PASS | DELETE with wrong x tag returns 401 or 403 |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-12 tests | |
| PASS | GET /list/<pubkey> returns 200 | |
| PASS | GET /list/<pubkey> returns JSON array | |
| PASS | blob descriptors contain required fields — list succeeds | |
| PASS | list has at least one entry | |
| PASS | list descriptor has url | |
| PASS | list descriptor has sha256 | |
| PASS | list descriptor has size | |
| PASS | list descriptor has type | |
| PASS | list descriptor has uploaded | |
| PASS | GET /list/<pubkey>?limit=1 returns 200 | |
| PASS | limit=1 returns at most 1 result | |
| PASS | cursor pagination — initial list succeeds | |
| PASS | initial list has entries | |
| PASS | cursor pagination — second page returns 200 | |
| WARN | cursor excludes previous entry | |
| PASS | results sorted by uploaded date descending — list succeeds | |
| WARN | GET /list returns 400 for malformed query params HTTP 200 | Body: [{"sha256":"73dddd4952f8a2bc426a1a519dacdc0d4b29e955ac1bcac2549ed836a3cbbcb9","size":67622,"uploaded":1777471183,"type":"image/png","url":"https://blossom.yakihonne.com/73dddd4952f8a2bc426a1a519dacdc0 | |
| PASS AUTH | delete test — upload setup succeeds | |
| PASS | delete returns 200 or 204 on success | |
| WARN | deleted blob returns 404 on GET HTTP 200 | |
| PASS | delete non-existent blob returns 404 | |
| PASS | delete without authorization returns 401 | |
| PASS | delete requires x tag — upload succeeds | |
| PASS | delete with wrong x tag returns 401 or 403 | |
| PASS | multiple x tags — delete blob1 succeeds | |
| PASS | multiple x tags do not delete multiple blobs |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| FAIL | GET response includes Access-Control-Allow-Origin: * access-control-allow-origin: (missing) | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Headers access-control-allow-headers: (missing) | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Methods access-control-allow-methods: (missing) | |
| FAIL | error response has 4xx/5xx status HTTP 200 | Body: <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/jpg" href="./route96.jpg" />
<meta name="viewport" content="width=device-width, initial-scal | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| FAIL | Access-Control-Allow-Origin: * on GET access-control-allow-origin: (missing) | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | accept-ranges contains 'bytes' | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| PASS | Content-Type defaults to application/octet-stream — upload succeeds | |
| PASS | Content-Type defaults to application/octet-stream — GET succeeds | |
| PASS | Content-Type header present for octet-stream blob | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 201 | Body: {"url":"https://nostr.download/3e619869473d7b5b6afceb78c8cfe45a74203bcf878bf5e6d409c1996ac160cf.png","sha256":"3e619869473d7b5b6afceb78c8cfe45a74203bcf878bf5e6d409c1996ac160cf","size":69387,"type":"im | |
| PASS | descriptor contains required fields — upload succeeds | |
| PASS | descriptor is valid JSON with required fields | |
| PASS | descriptor has url field | |
| PASS | descriptor has sha256 field | |
| PASS | descriptor has size field | |
| PASS | descriptor has type field | |
| PASS AUTH | descriptor has uploaded field | |
| PASS | descriptor sha256 matches — upload succeeds | |
| PASS | descriptor sha256 matches uploaded blob hash | |
| PASS | descriptor size matches — upload succeeds | |
| PASS | descriptor size is greater than 0 | |
| PASS | descriptor type reflects MIME — upload succeeds | |
| PASS | descriptor has type field | |
| PASS | descriptor uploaded is timestamp — upload succeeds | |
| PASS | descriptor uploaded is an integer | |
| PASS | descriptor uploaded > 0 | |
| PASS | descriptor url includes extension — upload succeeds | |
| PASS | descriptor URL includes file extension | |
| PASS | descriptor type falls back for unknown — upload succeeds | |
| PASS | descriptor has type field for unknown MIME | |
| PASS AUTH | new blob returns 201 Created | |
| PASS | duplicate blob — first upload succeeds | |
| PASS | duplicate blob returns 200 OK | |
| PASS | server does not modify blob — upload succeeds | |
| PASS | server does not modify blob — GET succeeds | |
| PASS | downloaded blob hash matches original | |
| PASS | X-SHA-256 header is accepted | |
| PASS AUTH | upload works without prior HEAD /upload request |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-04 tests | |
| PASS | mirror endpoint accepts URL in request body | |
| PASS | mirrored blob returns blob descriptor | |
| PASS | mirror descriptor sha256 matches original | |
| PASS | mirror descriptor has url | |
| PASS | mirror descriptor has size | |
| PASS | mirror descriptor has type | |
| PASS | mirror descriptor has uploaded | |
| PASS | existing mirror returns 200 OK | |
| PASS | malformed request body returns 400 | |
| WARN | missing url in body returns 400 HTTP 422 | Body: Failed to deserialize the JSON body into the target type: missing field `url` at line 1 column 2 | |
| PASS | unreachable URL returns 502 Bad Gateway |
| Test | ||
|---|---|---|
| PASS AUTH | PUT /media accepts binary data and returns 200 or 201 | |
| PASS | PUT /media returns blob descriptor on success | |
| PASS | media descriptor has url | |
| PASS | media descriptor has sha256 | |
| PASS | media descriptor has size | |
| PASS | media descriptor has type | |
| PASS | media descriptor has uploaded | |
| PASS | PUT /media X-SHA-256 header is accepted | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 201 | Body: {"url":"https://nostr.download/f5c1c9f3428bde59faf9b977e86b40b10b0960bebbcee767e4c8da9ee1be5e5c.webp","sha256":"f5c1c9f3428bde59faf9b977e86b40b10b0960bebbcee767e4c8da9ee1be5e5c","size":17718,"type":"i | |
| WARN | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers HTTP 204 | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 204 | |
| PASS | HEAD /media X-Reason header on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable |
| Test | ||
|---|---|---|
| WARN | HEAD /upload accepts headers and returns 200 HTTP 204 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 204 | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 204 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 204 | |
| PASS | HEAD /upload too large X-Content-Length returns 413 | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| WARN | preflight 200 allows subsequent PUT upload HTTP 204 | |
| PASS AUTH | preflight followed by PUT upload succeeds |
| Test | ||
|---|---|---|
| PASS AUTH | 402 Payment Required — server does not require payment | |
| PASS | X-Cashu — server does not require payment | |
| PASS | X-Lightning — server does not require payment |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-08 tests | |
| PASS | nip94 field is a JSON array when present | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 entry is an array | |
| PASS | nip94 entry has length >= 2 | |
| PASS | nip94 entry key is a string | |
| PASS | nip94 contains url tag | |
| PASS | nip94 url tag matches descriptor url | |
| PASS | nip94 contains x tag matching sha256 | |
| PASS | nip94 x tag matches sha256 | |
| PASS | nip94 contains m tag matching MIME type | |
| PASS | nip94 m tag matches type | |
| PASS | mirror response nip94 test — mirror succeeds | |
| PASS | mirror response nip94 field is a JSON array when present |
| Test | ||
|---|---|---|
| PASS AUTH | PUT /report accepts signed kind:1984 report event | |
| PASS AUTH | PUT /report with multiple x tags is accepted | |
| PASS AUTH | PUT /report with e/p tags is accepted | |
| PASS AUTH | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| PASS AUTH | GET /<sha256> with t=get auth — upload setup succeeds | |
| PASS | GET /<sha256> with t=get auth returns 200 | |
| PASS AUTH | PUT /upload with t=upload auth succeeds | |
| PASS | DELETE /<sha256> with t=delete auth succeeds | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| PASS | PUT /mirror with t=upload auth succeeds | |
| PASS AUTH | PUT /media with t=media auth succeeds | |
| PASS | DELETE without authorization returns 401 | |
| PASS | DELETE with wrong x tag returns 401 or 403 |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-12 tests | |
| PASS | GET /list/<pubkey> returns 200 | |
| PASS | GET /list/<pubkey> returns JSON array | |
| PASS | blob descriptors contain required fields — list succeeds | |
| PASS | list has at least one entry | |
| PASS | list descriptor has url | |
| PASS | list descriptor has sha256 | |
| PASS | list descriptor has size | |
| PASS | list descriptor has type | |
| PASS | list descriptor has uploaded | |
| PASS | GET /list/<pubkey>?limit=1 returns 200 | |
| PASS | limit=1 returns at most 1 result | |
| PASS | cursor pagination — initial list succeeds | |
| PASS | initial list has entries | |
| PASS | cursor pagination — second page returns 200 | |
| PASS | cursor excludes previous entry | |
| PASS | results sorted by uploaded date descending — list succeeds | |
| PASS | GET /list returns 400 for malformed query params | |
| PASS AUTH | delete test — upload setup succeeds | |
| PASS | delete returns 200 or 204 on success | |
| PASS | deleted blob returns 404 on GET | |
| PASS | delete non-existent blob returns 404 | |
| PASS | delete without authorization returns 401 | |
| PASS | delete requires x tag — upload succeeds | |
| PASS | delete with wrong x tag returns 401 or 403 | |
| PASS | multiple x tags — delete blob1 succeeds | |
| PASS | multiple x tags do not delete multiple blobs |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Headers access-control-allow-headers: (missing) | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Methods access-control-allow-methods: (missing) | |
| PASS | error response has 4xx/5xx status | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| PASS | Access-Control-Allow-Origin: * on GET | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | 206 response has Content-Range header | |
| PASS | Content-Range contains 'bytes' | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| PASS | Content-Type defaults to application/octet-stream — upload succeeds | |
| PASS | Content-Type defaults to application/octet-stream — GET succeeds | |
| PASS | Content-Type header present for octet-stream blob | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request | |
| PASS | descriptor contains required fields — upload succeeds | |
| PASS | descriptor is valid JSON with required fields | |
| PASS | descriptor has url field | |
| PASS | descriptor has sha256 field | |
| PASS | descriptor has size field | |
| PASS | descriptor has type field | |
| PASS AUTH | descriptor has uploaded field | |
| PASS | descriptor sha256 matches — upload succeeds | |
| PASS | descriptor sha256 matches uploaded blob hash | |
| PASS | descriptor size matches — upload succeeds | |
| PASS | descriptor size is greater than 0 | |
| PASS | descriptor type reflects MIME — upload succeeds | |
| PASS | descriptor has type field | |
| PASS | descriptor uploaded is timestamp — upload succeeds | |
| PASS | descriptor uploaded is an integer | |
| PASS | descriptor uploaded > 0 | |
| PASS | descriptor url includes extension — upload succeeds | |
| PASS | descriptor URL includes file extension | |
| PASS | descriptor type falls back for unknown — upload succeeds | |
| PASS | descriptor has type field for unknown MIME | |
| WARN AUTH | new blob returns 201 Created HTTP 200 | Body: {"sha256":"c69c224a10e9e16b15b723d0086d06410d1df37fc81777ba4833b20600ccd1ec","size":69135,"uploaded":1777471205,"type":"image/png","url":"https://milo.nostria.app/c69c224a10e9e16b15b723d0086d06410d1df | |
| PASS | duplicate blob — first upload succeeds | |
| PASS | duplicate blob returns 200 OK | |
| PASS | server does not modify blob — upload succeeds | |
| PASS | server does not modify blob — GET succeeds | |
| PASS | downloaded blob hash matches original | |
| PASS | X-SHA-256 header is accepted | |
| PASS AUTH | upload works without prior HEAD /upload request |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-04 tests | |
| PASS | mirror endpoint accepts URL in request body | |
| PASS | mirrored blob returns blob descriptor | |
| PASS | mirror descriptor sha256 matches original | |
| PASS | mirror descriptor has url | |
| PASS | mirror descriptor has size | |
| PASS | mirror descriptor has type | |
| PASS | mirror descriptor has uploaded | |
| PASS | existing mirror returns 200 OK | |
| PASS | malformed request body returns 400 | |
| PASS | missing url in body returns 400 | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 500 | X-Reason: Something went wrong | Body: Internal Server Error |
| Test | ||
|---|---|---|
| PASS AUTH | PUT /media accepts binary data and returns 200 or 201 | |
| PASS | PUT /media returns blob descriptor on success | |
| PASS | media descriptor has url | |
| PASS | media descriptor has sha256 | |
| PASS | media descriptor has size | |
| PASS | media descriptor has type | |
| PASS | media descriptor has uploaded | |
| PASS | PUT /media X-SHA-256 header is accepted | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 200 | |
| PASS | HEAD /media X-Reason header on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 200 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 200 | |
| PASS | HEAD /upload too large X-Content-Length returns 413 | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| PASS | preflight 200 allows subsequent PUT upload | |
| PASS AUTH | preflight followed by PUT upload succeeds |
| Test | ||
|---|---|---|
| PASS AUTH | 402 Payment Required — server does not require payment | |
| PASS | X-Cashu — server does not require payment | |
| PASS | X-Lightning — server does not require payment |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-08 tests | |
| PASS | mirror response nip94 test — mirror succeeds |
| Test | ||
|---|---|---|
| WARN | PUT /report accepts signed kind:1984 report event HTTP 405 | Body: Method Not Allowed | |
| WARN | PUT /report with multiple x tags is accepted HTTP 405 | Body: Method Not Allowed | |
| WARN | PUT /report with e/p tags is accepted HTTP 405 | Body: Method Not Allowed | |
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| PASS AUTH | GET /<sha256> with t=get auth — upload setup succeeds | |
| PASS | GET /<sha256> with t=get auth returns 200 | |
| PASS AUTH | PUT /upload with t=upload auth succeeds | |
| PASS | DELETE /<sha256> with t=delete auth succeeds | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| PASS | PUT /mirror with t=upload auth succeeds | |
| PASS AUTH | PUT /media with t=media auth succeeds | |
| PASS | DELETE without authorization returns 401 | |
| PASS | DELETE with wrong x tag returns 401 or 403 |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-12 tests | |
| PASS | GET /list/<pubkey> returns 200 | |
| PASS | GET /list/<pubkey> returns JSON array | |
| PASS | blob descriptors contain required fields — list succeeds | |
| PASS | list has at least one entry | |
| PASS | list descriptor has url | |
| PASS | list descriptor has sha256 | |
| PASS | list descriptor has size | |
| PASS | list descriptor has type | |
| PASS | list descriptor has uploaded | |
| PASS | GET /list/<pubkey>?limit=1 returns 200 | |
| PASS | limit=1 returns at most 1 result | |
| PASS | cursor pagination — initial list succeeds | |
| PASS | initial list has entries | |
| PASS | cursor pagination — second page returns 200 | |
| WARN | cursor excludes previous entry | |
| PASS | results sorted by uploaded date descending — list succeeds | |
| WARN | GET /list returns 400 for malformed query params HTTP 200 | Body: [{"sha256":"2443ae507c6c2cb798207af8c413ff784817043ea0c22a332bbdd4fdad102414","size":68841,"uploaded":1777471217,"type":"image/png","url":"https://milo.nostria.app/2443ae507c6c2cb798207af8c413ff784817 | |
| PASS AUTH | delete test — upload setup succeeds | |
| PASS | delete returns 200 or 204 on success | |
| PASS | deleted blob returns 404 on GET | |
| PASS | delete non-existent blob returns 404 | |
| PASS | delete without authorization returns 401 | |
| PASS | delete requires x tag — upload succeeds | |
| PASS | delete with wrong x tag returns 401 or 403 | |
| PASS | multiple x tags — delete blob1 succeeds | |
| PASS | multiple x tags do not delete multiple blobs |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| FAIL | upload blob for BUD-01 tests HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Headers | |
| FAIL | OPTIONS Allow-Headers includes Authorization access-control-allow-headers: * | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Methods | |
| PASS | OPTIONS Allow-Methods includes GET | |
| PASS | OPTIONS Allow-Methods includes HEAD | |
| PASS | OPTIONS Allow-Methods includes PUT | |
| FAIL | OPTIONS Allow-Methods includes DELETE | |
| FAIL | error response has 4xx/5xx status HTTP 200 | Body: <!DOCTYPE html><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><meta http-equiv="X-UA-Compatible" content="ie=edge" /><meta name="description" conten | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| FAIL | upload endpoint is accessible at /upload HTTP 403 | Body: {"status":"error","message":"Authorization required"} |
| Test | ||
|---|---|---|
| WARN | upload without X-SHA-256 returns 200/201 or 400 if required HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to upload files."} | |
| WARN | descriptor contains required fields — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | descriptor sha256 matches — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | descriptor size matches — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | descriptor type reflects MIME — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | descriptor uploaded is timestamp — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | descriptor url includes extension — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | new blob returns 201 Created HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | duplicate blob — first upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | duplicate blob returns 200 OK HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to upload files."} | |
| WARN | server does not modify blob — upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | X-SHA-256 header is accepted HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | upload works without prior HEAD /upload request HTTP 403 | Body: {"status":"error","message":"Authorization required"} |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-04 tests HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | malformed request body returns 400 HTTP 500 | Body: {"status":"error","message":"Unexpected token 'o', \"not json\" is not valid JSON"} | |
| PASS | missing url in body returns 400 | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to mirror files."} |
| Test | ||
|---|---|---|
| WARN AUTH | PUT /media accepts binary data and returns 200 or 201 HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"} | |
| WARN | PUT /media returns blob descriptor on success HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"} | |
| WARN | PUT /media X-SHA-256 header is accepted HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"} | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"} | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 200 | X-Reason: Missing auth handler | |
| WARN | HEAD /media X-Reason header on error — status is 4xx HTTP 200 | X-Reason: Missing auth handler | |
| PASS | X-Reason header on error is human-readable |
| Test | ||
|---|---|---|
| WARN | HEAD /upload accepts headers and returns 200 HTTP 401 | X-Reason: Missing auth handler | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 401 | X-Reason: Missing auth handler | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 401 | X-Reason: Missing auth handler | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 401 | X-Reason: Missing auth handler | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 401 | X-Reason: Missing auth handler | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| WARN | preflight 200 allows subsequent PUT upload HTTP 401 | X-Reason: Missing auth handler | |
| WARN | preflight followed by PUT upload succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} |
| Test | ||
|---|---|---|
| WARN | 402 Payment Required — server does not require payment HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | X-Cashu — server does not require payment HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | X-Lightning — server does not require payment HTTP 403 | Body: {"status":"error","message":"Authorization required"} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-08 tests HTTP 403 | Body: {"status":"error","message":"Authorization required"} |
| Test | ||
|---|---|---|
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| WARN | server accepts standard base64 encoded auth token (not base64url) HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to upload files."} | |
| WARN | GET /<sha256> with t=get auth — upload setup succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| WARN | PUT /upload with t=upload auth succeeds HTTP 403 | Body: {"status":"error","message":"Authorization required"} | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN AUTH | PUT /media with t=media auth succeeds HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-12 tests HTTP 403 | Body: {"status":"error","message":"Authorization required"} |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| FAIL | upload blob for BUD-01 tests HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Headers | |
| PASS | OPTIONS Allow-Headers includes Authorization | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Methods | |
| PASS | OPTIONS Allow-Methods includes GET | |
| FAIL | OPTIONS Allow-Methods includes HEAD | |
| PASS | OPTIONS Allow-Methods includes PUT | |
| PASS | OPTIONS Allow-Methods includes DELETE | |
| PASS | error response has 4xx/5xx status | |
| FAIL | GET /<sha256> returns 404 for non-existent blob HTTP 200 | X-Reason: File not found | |
| PASS | HEAD returns 404 for non-existent blob | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| FAIL | upload endpoint is accessible at /upload HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| WARN | upload without X-SHA-256 returns 200/201 or 400 if required HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor contains required fields — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor sha256 matches — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor size matches — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor type reflects MIME — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor uploaded is timestamp — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor url includes extension — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN AUTH | new blob returns 201 Created HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | duplicate blob — first upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | duplicate blob returns 200 OK HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | server does not modify blob — upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | X-SHA-256 header is accepted HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN AUTH | upload works without prior HEAD /upload request HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-04 tests HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| PASS | malformed request body returns 400 | |
| WARN | missing url in body returns 400 HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| WARN | PUT /media accepts binary data and returns 200 or 201 HTTP 404 | Body: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot PUT /media</pre>
</body>
</html> | |
| WARN | PUT /media returns blob descriptor on success HTTP 404 | Body: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot PUT /media</pre>
</body>
</html> | |
| WARN | PUT /media X-SHA-256 header is accepted HTTP 404 | Body: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot PUT /media</pre>
</body>
</html> | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 404 | Body: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot PUT /media</pre>
</body>
</html> | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /media X-Reason header on error — status is 4xx HTTP 200 |
| Test | ||
|---|---|---|
| WARN | HEAD /upload accepts headers and returns 200 HTTP 401 | X-Reason: Auth header server tag does not include this host | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 401 | X-Reason: Auth header server tag does not include this host | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 401 | X-Reason: Auth header server tag does not include this host | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 401 | X-Reason: Auth header server tag does not include this host | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 401 | X-Reason: Auth header server tag does not include this host | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| WARN | preflight 200 allows subsequent PUT upload HTTP 401 | X-Reason: Auth header server tag does not include this host | |
| WARN AUTH | preflight followed by PUT upload succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| WARN AUTH | 402 Payment Required — server does not require payment HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | X-Cashu — server does not require payment HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | X-Lightning — server does not require payment HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-08 tests HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| WARN | server accepts standard base64 encoded auth token (not base64url) HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | GET /<sha256> with t=get auth — upload setup succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| WARN | PUT /upload with t=upload auth succeeds HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /media with t=media auth succeeds HTTP 404 | Body: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot PUT /media</pre>
</body>
</html> |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-12 tests HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"} |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Headers | |
| PASS | OPTIONS Allow-Headers includes Authorization | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Methods | |
| PASS | OPTIONS Allow-Methods includes GET | |
| PASS | OPTIONS Allow-Methods includes HEAD | |
| PASS | OPTIONS Allow-Methods includes PUT | |
| PASS | OPTIONS Allow-Methods includes DELETE | |
| PASS | error response has 4xx/5xx status | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| PASS | Access-Control-Allow-Origin: * on GET | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | accept-ranges contains 'bytes' | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | 206 response has Content-Range header | |
| PASS | Content-Range contains 'bytes' | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| PASS | Content-Type defaults to application/octet-stream — upload succeeds | |
| PASS | Content-Type defaults to application/octet-stream — GET succeeds | |
| PASS | Content-Type header present for octet-stream blob | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| PASS | X-SHA-256 mismatch returns 409 Conflict | |
| PASS | descriptor contains required fields — upload succeeds | |
| PASS | descriptor is valid JSON with required fields | |
| PASS | descriptor has url field | |
| PASS | descriptor has sha256 field | |
| PASS | descriptor has size field | |
| PASS | descriptor has type field | |
| PASS AUTH | descriptor has uploaded field | |
| PASS | descriptor sha256 matches — upload succeeds | |
| PASS | descriptor sha256 matches uploaded blob hash | |
| PASS | descriptor size matches — upload succeeds | |
| PASS | descriptor size is greater than 0 | |
| PASS | descriptor type reflects MIME — upload succeeds | |
| PASS | descriptor has type field | |
| PASS | descriptor uploaded is timestamp — upload succeeds | |
| PASS | descriptor uploaded is an integer | |
| PASS | descriptor uploaded > 0 | |
| PASS | descriptor url includes extension — upload succeeds | |
| PASS | descriptor URL includes file extension | |
| PASS | descriptor type falls back for unknown — upload succeeds | |
| PASS | descriptor has type field for unknown MIME | |
| PASS AUTH | new blob returns 201 Created | |
| PASS | duplicate blob — first upload succeeds | |
| PASS | duplicate blob returns 200 OK | |
| PASS | server does not modify blob — upload succeeds | |
| PASS | server does not modify blob — GET succeeds | |
| PASS | downloaded blob hash matches original | |
| PASS | X-SHA-256 header is accepted | |
| PASS AUTH | upload works without prior HEAD /upload request |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-04 tests | |
| PASS | mirror endpoint accepts URL in request body | |
| PASS | mirrored blob returns blob descriptor | |
| PASS | mirror descriptor sha256 matches original | |
| PASS | mirror descriptor has url | |
| PASS | mirror descriptor has size | |
| PASS | mirror descriptor has type | |
| PASS | mirror descriptor has uploaded | |
| PASS | existing mirror returns 200 OK | |
| PASS | malformed request body returns 400 | |
| PASS | missing url in body returns 400 | |
| PASS | unreachable URL returns 502 Bad Gateway |
| Test | ||
|---|---|---|
| WARN AUTH | PUT /media accepts binary data and returns 200 or 201 HTTP 500 | X-Reason: Invalid cross-device link (os error 18): rename '/tmp/ffeaf788d044f803.webp' -> 'data/blobs/bb94978e8068045957d6fe63421edabef09e68c8fd3e1e59c08320451de46dd8.webp' | Body: Invalid cross-device link (os error 18): rename '/tmp/ffeaf788d044f803.webp' -> 'data/blobs/bb94978e8068045957d6fe63421edabef09e68c8fd3e1e59c08320451de46dd8.webp' | |
| WARN | PUT /media returns blob descriptor on success HTTP 500 | X-Reason: Invalid cross-device link (os error 18): rename '/tmp/1a6020bcfc6b9109.webp' -> 'data/blobs/d3b19a7cffb659d59d26f62017468b37499d22dd14be937330685141f5b7f340.webp' | Body: Invalid cross-device link (os error 18): rename '/tmp/1a6020bcfc6b9109.webp' -> 'data/blobs/d3b19a7cffb659d59d26f62017468b37499d22dd14be937330685141f5b7f340.webp' | |
| WARN | PUT /media X-SHA-256 header is accepted HTTP 500 | X-Reason: Invalid cross-device link (os error 18): rename '/tmp/fc526c115f18afca.webp' -> 'data/blobs/e75cca5c14f108bd3c3861f3b2822a6b9274519a1cc0c30513e39a2d4ccf76f3.webp' | Body: Invalid cross-device link (os error 18): rename '/tmp/fc526c115f18afca.webp' -> 'data/blobs/e75cca5c14f108bd3c3861f3b2822a6b9274519a1cc0c30513e39a2d4ccf76f3.webp' | |
| PASS | PUT /media X-SHA-256 mismatch returns 409 Conflict | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 200 | |
| PASS | HEAD /media X-Reason header on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable |
| Test | ||
|---|---|---|
| WARN | HEAD /upload accepts headers and returns 200 HTTP 204 | |
| PASS | HEAD /upload response has no body | |
| PASS | HEAD /upload missing X-Content-Length returns 411 | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 204 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 204 | |
| PASS | HEAD /upload too large X-Content-Length returns 413 | |
| PASS | HEAD /upload X-Reason on error — status is 4xx | |
| PASS | X-Reason header on error is human-readable | |
| WARN | preflight 200 allows subsequent PUT upload HTTP 204 | |
| PASS AUTH | preflight followed by PUT upload succeeds |
| Test | ||
|---|---|---|
| PASS AUTH | 402 Payment Required — server does not require payment | |
| PASS | X-Cashu — server does not require payment | |
| PASS | X-Lightning — server does not require payment |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-08 tests | |
| PASS | mirror response nip94 test — mirror succeeds |
| Test | ||
|---|---|---|
| PASS | PUT /report accepts signed kind:1984 report event | |
| PASS | PUT /report with multiple x tags is accepted | |
| WARN | PUT /report with invalid blob hash is rejected with 4xx/5xx HTTP 200 | Body: {"success":true} |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| PASS | server accepts standard base64 encoded auth token (not base64url) | |
| PASS AUTH | GET /<sha256> with t=get auth — upload setup succeeds | |
| PASS | GET /<sha256> with t=get auth returns 200 | |
| PASS AUTH | PUT /upload with t=upload auth succeeds | |
| PASS | DELETE /<sha256> with t=delete auth succeeds | |
| WARN | GET /list/<pubkey> with t=list auth returns 200 HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| PASS | PUT /mirror with t=upload auth succeeds | |
| WARN AUTH | PUT /media with t=media auth succeeds HTTP 500 | X-Reason: Invalid cross-device link (os error 18): rename '/tmp/f1abfe8485e956d2.webp' -> 'data/blobs/f682710434e5e5767244ae2e2918e2e3dd8235a47122a84f805d6dbb4200b4ba.webp' | Body: Invalid cross-device link (os error 18): rename '/tmp/f1abfe8485e956d2.webp' -> 'data/blobs/f682710434e5e5767244ae2e2918e2e3dd8235a47122a84f805d6dbb4200b4ba.webp' | |
| PASS | DELETE without authorization returns 401 | |
| PASS | DELETE with wrong x tag returns 401 or 403 |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-12 tests | |
| WARN | GET /list/<pubkey> returns 200 HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| WARN | blob descriptors contain required fields — list succeeds HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| WARN | GET /list/<pubkey>?limit=1 returns 200 HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| WARN | cursor pagination — initial list succeeds HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| WARN | results sorted by uploaded date descending — list succeeds HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| WARN | GET /list returns 400 for malformed query params HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server | |
| PASS AUTH | delete test — upload setup succeeds | |
| PASS | delete returns 200 or 204 on success | |
| PASS | deleted blob returns 404 on GET | |
| PASS | delete non-existent blob returns 404 | |
| PASS | delete without authorization returns 401 | |
| PASS | delete requires x tag — upload succeeds | |
| PASS | delete with wrong x tag returns 401 or 403 | |
| PASS | multiple x tags — delete blob1 succeeds | |
| PASS | multiple x tags do not delete multiple blobs |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| FAIL | OPTIONS preflight includes Access-Control-Allow-Headers access-control-allow-headers: (missing) | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Methods | |
| PASS | OPTIONS Allow-Methods includes GET | |
| PASS | OPTIONS Allow-Methods includes HEAD | |
| PASS | OPTIONS Allow-Methods includes PUT | |
| PASS | OPTIONS Allow-Methods includes DELETE | |
| PASS | error response has 4xx/5xx status | |
| FAIL | GET /<sha256> returns 404 for non-existent blob HTTP 200 | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| PASS | Access-Control-Allow-Origin: * on GET | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| FAIL | Content-Length header present content-length: (missing) | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 400 | Body: {"status":"error","message":"Empty file"} | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 202 | Body: {"status":"success","message":"","url":"https://cdn.sovbit.host/61c6a808a1fdfdb0e691899fa99ec36fb003c9810b2897c442751c91af75f42c.png","sha256":"61c6a808a1fdfdb0e691899fa99ec36fb003c9810b2897c442751c91 | |
| WARN | descriptor contains required fields — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | descriptor sha256 matches — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | descriptor size matches — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | descriptor type reflects MIME — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | descriptor uploaded is timestamp — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | descriptor url includes extension — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | new blob returns 201 Created HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | duplicate blob — first upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | duplicate blob returns 200 OK HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | server does not modify blob — upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | X-SHA-256 header is accepted HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | upload works without prior HEAD /upload request HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-04 tests HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | malformed request body returns 400 HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | missing url in body returns 400 HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| WARN | PUT /media accepts binary data and returns 200 or 201 HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | PUT /media returns blob descriptor on success HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | PUT /media X-SHA-256 header is accepted HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers HTTP 404 | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 404 | |
| PASS | HEAD /media X-Reason header on error — status is 4xx |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 400 | X-Reason: Missing size, MIME type or SHA-256 | |
| PASS | HEAD /upload missing X-SHA-256 returns 400 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 200 | X-Reason: Upload allowed | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 200 | X-Reason: Upload allowed | |
| WARN | HEAD /upload X-Reason on error — status is 4xx HTTP 200 | X-Reason: Upload allowed | |
| PASS | X-Reason header on error is human-readable | |
| PASS | preflight 200 allows subsequent PUT upload | |
| WARN | preflight followed by PUT upload succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| WARN | 402 Payment Required — server does not require payment HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | X-Cashu — server does not require payment HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | X-Lightning — server does not require payment HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-08 tests HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| WARN | server accepts standard base64 encoded auth token (not base64url) HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | GET /<sha256> with t=get auth — upload setup succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| WARN | PUT /upload with t=upload auth succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /media with t=media auth succeeds HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-12 tests HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."} |
| Test | ||
|---|---|---|
| PASS | server is reachable and responds to HTTP requests |
| Test | ||
|---|---|---|
| PASS AUTH | upload blob for BUD-01 tests | |
| PASS | GET response includes Access-Control-Allow-Origin: * | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Headers | |
| PASS | OPTIONS Allow-Headers includes Authorization | |
| PASS | OPTIONS preflight includes Access-Control-Allow-Methods | |
| PASS | OPTIONS Allow-Methods includes GET | |
| PASS | OPTIONS Allow-Methods includes HEAD | |
| PASS | OPTIONS Allow-Methods includes PUT | |
| PASS | OPTIONS Allow-Methods includes DELETE | |
| FAIL | error response has 4xx/5xx status HTTP 200 | Body: <!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/blossom.svg" />
<meta name="viewport" content="width=device-wid | |
| PASS | GET /<sha256> returns 404 for non-existent blob | |
| PASS | HEAD returns 404 for non-existent blob | |
| PASS | GET /<sha256> returns 200 OK | |
| PASS | GET /<sha256> response body matches uploaded blob — GET succeeds | |
| PASS | downloaded blob hash matches upload | |
| PASS | GET /<sha256> Content-Type header is set | |
| PASS | Content-Type header present | |
| PASS | GET /<sha256>.png accepts optional file extension | |
| PASS | CORS header present on GET response | |
| PASS | Access-Control-Allow-Origin: * on GET | |
| PASS | redirect URLs contain same sha256 hash (no redirect) | |
| PASS | HEAD /<sha256> returns 200 OK for existing blob | |
| PASS | HEAD returns same Content-Type as GET | |
| PASS | HEAD Content-Type matches GET Content-Type | |
| PASS | HEAD returns Content-Length header | |
| PASS | Content-Length header present | |
| PASS | Content-Length > 0 | |
| PASS | HEAD does not return blob body | |
| PASS | HEAD response body is empty | |
| PASS | HEAD accepts optional file extension in URL | |
| PASS | HEAD response includes accept-ranges: bytes | |
| PASS | accept-ranges contains 'bytes' | |
| PASS | GET with Range header returns 200 or 206 | |
| PASS | 206 response has Content-Range header | |
| PASS | Content-Range contains 'bytes' | |
| PASS | Sunset header test — GET succeeds | |
| PASS | blob retrieval endpoint is accessible at /<sha256> | |
| FAIL | Content-Type defaults to application/octet-stream — upload succeeds HTTP 415 | X-Reason: MIME type application/octet-stream is not supported | Body: {"message":"MIME type application/octet-stream is not supported"} | |
| PASS AUTH | upload endpoint is accessible at /upload |
| Test | ||
|---|---|---|
| WARN | X-SHA-256 mismatch returns 409 Conflict HTTP 400 | X-Reason: SHA-256 hash does not match expected value | Body: {"message":"SHA-256 hash does not match expected value"} | |
| WARN | descriptor contains required fields — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | descriptor sha256 matches — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | descriptor size matches — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | descriptor type reflects MIME — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | descriptor uploaded is timestamp — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | descriptor url includes extension — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | descriptor type falls back for unknown — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | new blob returns 201 Created HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | duplicate blob — first upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | duplicate blob returns 200 OK HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | server does not modify blob — upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | X-SHA-256 header is accepted HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | upload works without prior HEAD /upload request HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |
| Test | ||
|---|---|---|
| SKIP | BUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-04 tests HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | malformed request body returns 400 HTTP 500 | X-Reason: Something went wrong | Body: {"error":"INTERNAL_ERROR","message":"Unexpected token 'n', \"not json\" is not valid JSON"} | |
| WARN | missing url in body returns 400 HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | unreachable URL returns 502 Bad Gateway HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |
| Test | ||
|---|---|---|
| WARN | PUT /media accepts binary data and returns 200 or 201 HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | PUT /media returns blob descriptor on success HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | PUT /media X-SHA-256 header is accepted HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | PUT /media X-SHA-256 mismatch returns 409 Conflict HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| PASS | HEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers | |
| PASS | HEAD /media returns status code without response body | |
| WARN | HEAD /media missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /media X-Reason header on error — status is 4xx HTTP 200 |
| Test | ||
|---|---|---|
| PASS | HEAD /upload accepts headers and returns 200 | |
| PASS | HEAD /upload response has no body | |
| WARN | HEAD /upload missing X-Content-Length returns 411 HTTP 200 | |
| WARN | HEAD /upload missing X-SHA-256 returns 400 HTTP 200 | |
| WARN | HEAD /upload malformed X-SHA-256 returns 400 HTTP 200 | |
| WARN | HEAD /upload too large X-Content-Length returns 413 HTTP 200 | |
| WARN | HEAD /upload X-Reason on error — status is 4xx HTTP 200 | |
| PASS | preflight 200 allows subsequent PUT upload | |
| WARN | preflight followed by PUT upload succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |
| Test | ||
|---|---|---|
| WARN | 402 Payment Required — server does not require payment HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | X-Cashu — server does not require payment HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | X-Lightning — server does not require payment HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-08 tests HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |
| Test | ||
|---|---|---|
| PASS | PUT /report with invalid blob hash is rejected with 4xx/5xx |
| Test | ||
|---|---|---|
| SKIP | BUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined | client-side only, no server endpoints |
| Test | ||
|---|---|---|
| PASS | expired token is rejected by server | |
| PASS | wrong t tag verb is rejected | |
| WARN | server accepts standard base64 encoded auth token (not base64url) HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | GET /<sha256> with t=get auth — upload setup succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| WARN | PUT /upload with t=upload auth succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} | |
| PASS | GET /list/<pubkey> with t=list auth returns 200 | |
| WARN | PUT /media with t=media auth succeeds HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |
| Test | ||
|---|---|---|
| WARN | upload blob for BUD-12 tests HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."} |