Blossom Compliance Report

2026-04-29T14:01:07.868Z — 11 servers tested

72%
1307 tests total across 11 servers
944 passed · 38 failed · 303 warnings
Primal primal-blossom https://blossom.primal.net
31%
19/61
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
4 pass 5 fail
44%
Test
FAILupload blob for BUD-01 tests
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
PASSGET response includes Access-Control-Allow-Origin: *
FAILOPTIONS preflight includes Access-Control-Allow-Headers
Expected non-null value, got null
access-control-allow-headers: (missing)
FAILOPTIONS preflight includes Access-Control-Allow-Methods
Expected non-null value, got null
access-control-allow-methods: (missing)
PASSerror response has 4xx/5xx status
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
FAILupload endpoint is accessible at /upload
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
BUD-02 OPTIONAL
2 pass 13 warn
13%
Test
PASSupload without X-SHA-256 returns 200/201 or 400 if required
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 401
HTTP 401 | Body: invalid x tag
WARNdescriptor contains required fields — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNdescriptor sha256 matches — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNdescriptor size matches — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNdescriptor type reflects MIME — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNdescriptor uploaded is timestamp — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNdescriptor url includes extension — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNnew blob returns 201 Created
Expected 201, got 400
HTTP 400 | Body: missing auth event
WARNduplicate blob — first upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
PASSduplicate blob returns 200 OK
WARNserver does not modify blob — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNX-SHA-256 header is accepted
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNupload works without prior HEAD /upload request
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
2 pass 2 warn
50%
Test
WARNupload blob for BUD-04 tests
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
PASSmalformed request body returns 400
PASSmissing url in body returns 400
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 400
HTTP 400 | Body: download error
BUD-05 OPTIONAL
2 pass 6 warn
25%
Test
WARNPUT /media accepts binary data and returns 200 or 201
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNPUT /media returns blob descriptor on success
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNPUT /media X-SHA-256 header is accepted
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 401
HTTP 401 | Body: invalid action in auth event
WARNHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
Expected 200, got 401
HTTP 401
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 401
HTTP 401
PASSHEAD /media X-Reason header on error — status is 4xx
BUD-06 OPTIONAL
3 pass 6 warn
33%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 401
HTTP 401
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 401
HTTP 401
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 200
HTTP 200
WARNHEAD /upload X-Reason on error — status is 4xx
Expected 200 > 399
HTTP 200
PASSpreflight 200 allows subsequent PUT upload
WARNpreflight followed by PUT upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
BUD-07 OPTIONAL
0 pass 3 warn
0%
Test
WARN402 Payment Required — server does not require payment
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNX-Cashu — server does not require payment
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNX-Lightning — server does not require payment
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
BUD-08 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-08 tests
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
BUD-09 OPTIONAL
1 pass
100%
Test
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
4 pass 3 warn
57%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
WARNGET /<sha256> with t=get auth — upload setup succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
WARNPUT /upload with t=upload auth succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /media with t=media auth succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
BUD-12 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-12 tests
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: missing auth event
24242.io 24242 https://24242.io
83%
119/144
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
31 pass 3 fail
91%
Test
PASS AUTHupload blob for BUD-01 tests
PASSGET response includes Access-Control-Allow-Origin: *
FAILOPTIONS preflight includes Access-Control-Allow-Headers
Expected non-null value, got null
access-control-allow-headers: (missing)
FAILOPTIONS preflight includes Access-Control-Allow-Methods
Expected non-null value, got null
access-control-allow-methods: (missing)
PASSerror response has 4xx/5xx status
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
PASSAccess-Control-Allow-Origin: * on GET
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSaccept-ranges contains 'bytes'
PASSGET with Range header returns 200 or 206
PASS206 response has Content-Range header
PASSContent-Range contains 'bytes'
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: unsupported content type: application/octet-stream | Body: unsupported content type: application/octet-stream
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
24 pass 4 warn
86%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 401
HTTP 401 | X-Reason: obj hashed wrong once uploaded, got d79c664adbeaba10fa1fd1f31d25dbcc8862684ba9f1b7c718510b017a5fd211 | Body: obj hashed wrong once uploaded, got d79c664adbeaba10fa1fd1f31d25dbcc8862684ba9f1b7c718510b017a5fd211
PASSdescriptor contains required fields — upload succeeds
PASSdescriptor is valid JSON with required fields
PASSdescriptor has url field
PASSdescriptor has sha256 field
PASSdescriptor has size field
PASSdescriptor has type field
PASS AUTHdescriptor has uploaded field
PASSdescriptor sha256 matches — upload succeeds
PASSdescriptor sha256 matches uploaded blob hash
PASSdescriptor size matches — upload succeeds
PASSdescriptor size is greater than 0
PASSdescriptor type reflects MIME — upload succeeds
PASSdescriptor has type field
PASSdescriptor uploaded is timestamp — upload succeeds
PASSdescriptor uploaded is an integer
PASSdescriptor uploaded > 0
PASSdescriptor url includes extension — upload succeeds
PASSdescriptor URL includes file extension
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: unsupported content type: application/octet-stream | Body: unsupported content type: application/octet-stream
WARN AUTHnew blob returns 201 Created
Expected 201, got 200
HTTP 200 | Body: {"type":"image\/png","size":66289,"owner":"87b45e681cb8ce5e57fd2225bf0033396ade8468f76b9caec6421ee9d0670c23","sha256":"de3ad7a310929354c731302aeb2f7c3c4b21305f6105ebb630eb49b15423459e","uploaded":1777
PASSduplicate blob — first upload succeeds
WARNduplicate blob returns 200 OK
Expected 200, got 400
HTTP 400 | X-Reason: already got that file | Body: already got that file
PASSserver does not modify blob — upload succeeds
PASSserver does not modify blob — GET succeeds
PASSdownloaded blob hash matches original
PASSX-SHA-256 header is accepted
PASS AUTHupload works without prior HEAD /upload request
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
1 pass 6 warn
14%
Test
PASS AUTHupload blob for BUD-04 tests
WARNmirror endpoint accepts URL in request body
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: already got that file | Body: already got that file
WARNmirrored blob returns blob descriptor
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: already got that file | Body: already got that file
WARNexisting mirror returns 200 OK
Expected 200, got 400
HTTP 400 | X-Reason: already got that file | Body: already got that file
WARNmalformed request body returns 400
Expected 400, got 401
HTTP 401 | X-Reason: expect URL in json of body | Body: expect URL in json of body
WARNmissing url in body returns 400
Expected 400, got 401
HTTP 401 | X-Reason: expect URL in json of body | Body: expect URL in json of body
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 400
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
BUD-05 OPTIONAL
12 pass 2 warn
86%
Test
PASS AUTHPUT /media accepts binary data and returns 200 or 201
PASSPUT /media returns blob descriptor on success
PASSmedia descriptor has url
PASSmedia descriptor has sha256
PASSmedia descriptor has size
PASSmedia descriptor has type
PASSmedia descriptor has uploaded
PASSPUT /media X-SHA-256 header is accepted
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 401
HTTP 401 | X-Reason: obj hashed wrong once uploaded, got 684474079fae8fe734033bb704a8dd89663f0f8d2ba9c8559a668754b9fe0c14 | Body: obj hashed wrong once uploaded, got 684474079fae8fe734033bb704a8dd89663f0f8d2ba9c8559a668754b9fe0c14
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 400
HTTP 400 | X-Reason: missing X-Content-Size header
PASSHEAD /media X-Reason header on error — status is 4xx
PASSX-Reason header on error is human-readable
BUD-06 OPTIONAL
8 pass 2 warn
80%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 400
HTTP 400 | X-Reason: missing X-Content-Size header
PASSHEAD /upload missing X-SHA-256 returns 400
PASSHEAD /upload malformed X-SHA-256 returns 400
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 400
HTTP 400 | X-Reason: size too big
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
PASSpreflight 200 allows subsequent PUT upload
PASS AUTHpreflight followed by PUT upload succeeds
BUD-07 OPTIONAL
3 pass
100%
Test
PASS AUTH402 Payment Required — server does not require payment
PASSX-Cashu — server does not require payment
PASSX-Lightning — server does not require payment
BUD-08 OPTIONAL
1 pass 1 warn
50%
Test
PASS AUTHupload blob for BUD-08 tests
WARNmirror response nip94 test — mirror succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: already got that file | Body: already got that file
BUD-09 OPTIONAL
3 pass 1 warn
75%
Test
PASSPUT /report accepts signed kind:1984 report event
PASSPUT /report with multiple x tags is accepted
PASSPUT /report with e/p tags is accepted
WARNPUT /report with invalid blob hash is rejected with 4xx/5xx
Expected 200 > 399
HTTP 200 | Body: Thanks for this report, it is recorded for processing.
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
10 pass 2 warn
83%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
PASS AUTHGET /<sha256> with t=get auth — upload setup succeeds
PASSGET /<sha256> with t=get auth returns 200
PASS AUTHPUT /upload with t=upload auth succeeds
PASSDELETE /<sha256> with t=delete auth succeeds
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /mirror with t=upload auth succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: already got that file | Body: already got that file
PASS AUTHPUT /media with t=media auth succeeds
WARNDELETE without authorization returns 401
Expected 401, got 502
HTTP 502 | Body: 502 error from 24242.io (level 0)
PASSDELETE with wrong x tag returns 401 or 403
BUD-12 OPTIONAL
25 pass 2 warn
93%
Test
PASS AUTHupload blob for BUD-12 tests
PASSGET /list/<pubkey> returns 200
PASSGET /list/<pubkey> returns JSON array
PASSblob descriptors contain required fields — list succeeds
PASSlist has at least one entry
PASSlist descriptor has url
PASSlist descriptor has sha256
PASSlist descriptor has size
PASSlist descriptor has type
PASSlist descriptor has uploaded
PASSGET /list/<pubkey>?limit=1 returns 200
PASSlimit=1 returns at most 1 result
PASScursor pagination — initial list succeeds
PASSinitial list has entries
PASScursor pagination — second page returns 200
WARNcursor excludes previous entry
Expected false, got true
PASSresults sorted by uploaded date descending — list succeeds
WARNGET /list returns 400 for malformed query params
Expected 400, got 200
HTTP 200 | Body: [ {"sha256":"6db16858768b93df1082fd5a78529b9402e96be25a44dcea22928bc4154bdc94","size":67178,"owner":"270a6807a4c60a8b35b1401c12ee388dcae996c6441675aba0259f75c553ac8f","type":"image\/png","uploaded":1
PASS AUTHdelete test — upload setup succeeds
PASSdelete returns 200 or 204 on success
PASSdeleted blob returns 404 on GET
PASSdelete non-existent blob returns 404
PASSdelete without authorization returns 401
PASSdelete requires x tag — upload succeeds
PASSdelete with wrong x tag returns 401 or 403
PASSmultiple x tags — delete blob1 succeeds
PASSmultiple x tags do not delete multiple blobs
blossom.band nostr-build https://blossom.band
83%
152/183
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
36 pass 3 fail
92%
Test
PASS AUTHupload blob for BUD-01 tests
FAILGET response includes Access-Control-Allow-Origin: *
Expected "*", got null
access-control-allow-origin: (missing)
PASSOPTIONS preflight includes Access-Control-Allow-Headers
PASSOPTIONS Allow-Headers includes Authorization
PASSOPTIONS preflight includes Access-Control-Allow-Methods
PASSOPTIONS Allow-Methods includes GET
PASSOPTIONS Allow-Methods includes HEAD
PASSOPTIONS Allow-Methods includes PUT
PASSOPTIONS Allow-Methods includes DELETE
PASSerror response has 4xx/5xx status
PASSX-Reason header is non-empty when present
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
FAILAccess-Control-Allow-Origin: * on GET
Expected "*", got null
access-control-allow-origin: (missing)
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSGET with Range header returns 200 or 206
PASS206 response has Content-Range header
PASSContent-Range contains 'bytes'
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 415
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 AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
25 pass 3 warn
89%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: Error uploading the file: File hash mismatch | Body: Error uploading the file: File hash mismatch
PASSdescriptor contains required fields — upload succeeds
PASSdescriptor is valid JSON with required fields
PASSdescriptor has url field
PASSdescriptor has sha256 field
PASSdescriptor has size field
PASSdescriptor has type field
PASS AUTHdescriptor has uploaded field
PASSdescriptor sha256 matches — upload succeeds
PASSdescriptor sha256 matches uploaded blob hash
PASSdescriptor size matches — upload succeeds
PASSdescriptor size is greater than 0
PASSdescriptor type reflects MIME — upload succeeds
PASSdescriptor has type field
PASSdescriptor uploaded is timestamp — upload succeeds
PASSdescriptor uploaded is an integer
PASSdescriptor uploaded > 0
PASSdescriptor url includes extension — upload succeeds
PASSdescriptor URL includes file extension
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 415
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 AUTHnew blob returns 201 Created
Expected 201, got 200
HTTP 200 | Body: {"url":"https://npub14axdszarth5a929mjkpukztn5l6pz3np392wxpftzc6muhml79dscs29wm.blossom.band/c84e1bdabe36f4290a3966d21b611195b836b05a336d1572504cc5db2c3d96b5.png","size":68167,"type":"image/png","sha2
PASSduplicate blob — first upload succeeds
PASSduplicate blob returns 200 OK
PASSserver does not modify blob — upload succeeds
PASSserver does not modify blob — GET succeeds
PASSdownloaded blob hash matches original
PASSX-SHA-256 header is accepted
PASS AUTHupload works without prior HEAD /upload request
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
2 pass 5 warn
29%
Test
PASS AUTHupload blob for BUD-04 tests
WARNmirror endpoint accepts URL in request body
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL
WARNmirrored blob returns blob descriptor
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL
WARNexisting mirror returns 200 OK
Expected 200, got 400
HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL
WARNmalformed request body returns 400
Expected 400, got 500
HTTP 500 | X-Reason: Internal Server Error | Body: Unexpected token 'o', "not json" is not valid JSON
PASSmissing url in body returns 400
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 400
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
BUD-05 OPTIONAL
12 pass 2 warn
86%
Test
PASS AUTHPUT /media accepts binary data and returns 200 or 201
PASSPUT /media returns blob descriptor on success
PASSmedia descriptor has url
PASSmedia descriptor has sha256
PASSmedia descriptor has size
PASSmedia descriptor has type
PASSmedia descriptor has uploaded
PASSPUT /media X-SHA-256 header is accepted
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: Error uploading the file: File hash mismatch | Body: Error uploading the file: File hash mismatch
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 400
HTTP 400 | X-Reason: Missing one or more required header(s): x-content-length, x-content-type, x-sha-256
PASSHEAD /media X-Reason header on error — status is 4xx
PASSX-Reason header on error is human-readable
BUD-06 OPTIONAL
8 pass 2 warn
80%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 400
HTTP 400 | X-Reason: Missing one or more required header(s): x-content-length, x-content-type, x-sha-256
PASSHEAD /upload missing X-SHA-256 returns 400
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
PASSHEAD /upload too large X-Content-Length returns 413
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
PASSpreflight 200 allows subsequent PUT upload
PASS AUTHpreflight followed by PUT upload succeeds
BUD-07 OPTIONAL
3 pass
100%
Test
PASS AUTH402 Payment Required — server does not require payment
PASSX-Cashu — server does not require payment
PASSX-Lightning — server does not require payment
BUD-08 OPTIONAL
34 pass 2 warn
94%
Test
PASS AUTHupload blob for BUD-08 tests
PASSnip94 field is a JSON array when present
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 contains url tag
WARNnip94 url tag matches descriptor url
Expected "https://npub13v7t9pgd7erg994sxelh8m3gjc5ddlcl0hrj2x2260c8agr8speqvktuuq.blossom.band/1dd17733bcfe1815a676b1d2edee5a0b7348da60cd4a1a9a2d7500ed93980ffc.png", got "https://image.nostr.build/1dd17733bcfe1815a676b1d2edee5a0b7348da60cd4a1a9a2d7500ed93980ffc.png"
PASSnip94 contains x tag matching sha256
PASSnip94 x tag matches sha256
PASSnip94 contains m tag matching MIME type
PASSnip94 m tag matches type
WARNmirror response nip94 test — mirror succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL
BUD-09 OPTIONAL
1 pass 3 warn
25%
Test
WARNPUT /report accepts signed kind:1984 report event
Expected one of [200, 201, 204], got 404
HTTP 404 | Body: Not Found /blossom.band/report
WARNPUT /report with multiple x tags is accepted
Expected one of [200, 201, 204], got 404
HTTP 404 | Body: Not Found /blossom.band/report
WARNPUT /report with e/p tags is accepted
Expected one of [200, 201, 204], got 404
HTTP 404 | Body: Not Found /blossom.band/report
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
10 pass 2 warn
83%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
PASS AUTHGET /<sha256> with t=get auth — upload setup succeeds
PASSGET /<sha256> with t=get auth returns 200
PASS AUTHPUT /upload with t=upload auth succeeds
PASSDELETE /<sha256> with t=delete auth succeeds
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /mirror with t=upload auth succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | X-Reason: Invalid mirroring URL | Body: Invalid mirroring URL
PASS AUTHPUT /media with t=media auth succeeds
PASSDELETE without authorization returns 401
WARNDELETE with wrong x tag returns 401 or 403
Expected one of [401, 403], got 400
HTTP 400 | X-Reason: Invalid x tag in Blossom Authorization event | Body: Invalid x tag in Blossom Authorization event
BUD-12 OPTIONAL
20 pass 7 warn
74%
Test
PASS AUTHupload blob for BUD-12 tests
PASSGET /list/<pubkey> returns 200
PASSGET /list/<pubkey> returns JSON array
PASSblob descriptors contain required fields — list succeeds
PASSlist has at least one entry
PASSlist descriptor has url
PASSlist descriptor has sha256
PASSlist descriptor has size
PASSlist descriptor has type
PASSlist descriptor has uploaded
PASSGET /list/<pubkey>?limit=1 returns 200
PASSlimit=1 returns at most 1 result
PASScursor pagination — initial list succeeds
PASSinitial list has entries
PASScursor pagination — second page returns 200
WARNcursor excludes previous entry
Expected false, got true
PASSresults sorted by uploaded date descending — list succeeds
WARNGET /list returns 400 for malformed query params
Expected 400, got 200
HTTP 200 | Body: [{"url":"https://npub1zqhlfs5xsd4zgnd23alwh5d2kwjj00wzfpvwlvz5dz488jdgfcms502nkp.blossom.band/5cb1d7c4a0b9e917da46df37bd06a2055c5133bc21cb90d81b904e46578159df.png","sha256":"5cb1d7c4a0b9e917da46df37bd
WARNdelete test — upload setup succeeds
Expected one of [200, 201, 202], got 429
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-
PASSdelete returns 200 or 204 on success
PASSdeleted blob returns 404 on GET
WARNdelete non-existent blob returns 404
Expected 404, got 204
HTTP 204
PASSdelete without authorization returns 401
WARNdelete requires x tag — upload succeeds
Expected one of [200, 201, 202], got 429
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-
WARNdelete with wrong x tag returns 401 or 403
Expected one of [401, 403], got 400
HTTP 400 | X-Reason: Invalid x tag in Blossom Authorization event | Body: Invalid x tag in Blossom Authorization event
PASSmultiple x tags — delete blob1 succeeds
WARNmultiple x tags do not delete multiple blobs
Expected 200, got 404
HTTP 404 | Body: Not Found
YakiHonne yakihonne https://blossom.yakihonne.com
79%
111/141
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
30 pass 3 fail
91%
Test
PASS AUTHupload blob for BUD-01 tests
PASSGET response includes Access-Control-Allow-Origin: *
FAILOPTIONS preflight includes Access-Control-Allow-Headers
Expected non-null value, got null
access-control-allow-headers: (missing)
FAILOPTIONS preflight includes Access-Control-Allow-Methods
Expected non-null value, got null
access-control-allow-methods: (missing)
PASSerror response has 4xx/5xx status
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
PASSAccess-Control-Allow-Origin: * on GET
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSGET with Range header returns 200 or 206
PASS206 response has Content-Range header
PASSContent-Range contains 'bytes'
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | X-Reason: Server dose not accept application/octet-stream blobs | Body: Unauthorized
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
25 pass 3 warn
89%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request
PASSdescriptor contains required fields — upload succeeds
PASSdescriptor is valid JSON with required fields
PASSdescriptor has url field
PASSdescriptor has sha256 field
PASSdescriptor has size field
PASSdescriptor has type field
PASS AUTHdescriptor has uploaded field
PASSdescriptor sha256 matches — upload succeeds
PASSdescriptor sha256 matches uploaded blob hash
PASSdescriptor size matches — upload succeeds
PASSdescriptor size is greater than 0
PASSdescriptor type reflects MIME — upload succeeds
PASSdescriptor has type field
PASSdescriptor uploaded is timestamp — upload succeeds
PASSdescriptor uploaded is an integer
PASSdescriptor uploaded > 0
PASSdescriptor url includes extension — upload succeeds
PASSdescriptor URL includes file extension
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | X-Reason: Server dose not accept application/octet-stream blobs | Body: Unauthorized
WARN AUTHnew blob returns 201 Created
Expected 201, got 200
HTTP 200 | Body: {"sha256":"d76583ebbbaca1518f42306f447f62cb0383a0ddb25c117f2662e59ea420949e","size":68973,"uploaded":1777471152,"type":"image/png","url":"https://blossom.yakihonne.com/d76583ebbbaca1518f42306f447f62cb
PASSduplicate blob — first upload succeeds
PASSduplicate blob returns 200 OK
PASSserver does not modify blob — upload succeeds
PASSserver does not modify blob — GET succeeds
PASSdownloaded blob hash matches original
PASSX-SHA-256 header is accepted
PASS AUTHupload works without prior HEAD /upload request
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
1 pass 6 warn
14%
Test
PASS AUTHupload blob for BUD-04 tests
WARNmirror endpoint accepts URL in request body
Expected one of [200, 201, 202], got 403
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>
WARNmirrored blob returns blob descriptor
Expected one of [200, 201, 202], got 403
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>
WARNexisting mirror returns 200 OK
Expected 200, got 403
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>
WARNmalformed request body returns 400
Expected 400, got 403
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>
WARNmissing url in body returns 400
Expected 400, got 403
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>
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 403
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>
BUD-05 OPTIONAL
10 pass 3 warn
77%
Test
PASS AUTHPUT /media accepts binary data and returns 200 or 201
PASSPUT /media returns blob descriptor on success
PASSmedia descriptor has url
PASSmedia descriptor has sha256
PASSmedia descriptor has size
PASSmedia descriptor has type
PASSmedia descriptor has uploaded
PASSPUT /media X-SHA-256 header is accepted
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /media X-Reason header on error — status is 4xx
Expected 200 > 399
HTTP 200
BUD-06 OPTIONAL
4 pass 5 warn
44%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 200
HTTP 200
WARNHEAD /upload X-Reason on error — status is 4xx
Expected 200 > 399
HTTP 200
PASSpreflight 200 allows subsequent PUT upload
PASS AUTHpreflight followed by PUT upload succeeds
BUD-07 OPTIONAL
3 pass
100%
Test
PASS AUTH402 Payment Required — server does not require payment
PASSX-Cashu — server does not require payment
PASSX-Lightning — server does not require payment
BUD-08 OPTIONAL
1 pass 1 warn
50%
Test
PASS AUTHupload blob for BUD-08 tests
WARNmirror response nip94 test — mirror succeeds
Expected one of [200, 201, 202], got 403
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>
BUD-09 OPTIONAL
1 pass 3 warn
25%
Test
WARNPUT /report accepts signed kind:1984 report event
Expected one of [200, 201, 204], got 405
HTTP 405 | Body: Method Not Allowed
WARNPUT /report with multiple x tags is accepted
Expected one of [200, 201, 204], got 405
HTTP 405 | Body: Method Not Allowed
WARNPUT /report with e/p tags is accepted
Expected one of [200, 201, 204], got 405
HTTP 405 | Body: Method Not Allowed
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
11 pass 1 warn
92%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
PASS AUTHGET /<sha256> with t=get auth — upload setup succeeds
PASSGET /<sha256> with t=get auth returns 200
PASS AUTHPUT /upload with t=upload auth succeeds
PASSDELETE /<sha256> with t=delete auth succeeds
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /mirror with t=upload auth succeeds
Expected one of [200, 201, 202], got 403
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 AUTHPUT /media with t=media auth succeeds
PASSDELETE without authorization returns 401
PASSDELETE with wrong x tag returns 401 or 403
BUD-12 OPTIONAL
24 pass 3 warn
89%
Test
PASS AUTHupload blob for BUD-12 tests
PASSGET /list/<pubkey> returns 200
PASSGET /list/<pubkey> returns JSON array
PASSblob descriptors contain required fields — list succeeds
PASSlist has at least one entry
PASSlist descriptor has url
PASSlist descriptor has sha256
PASSlist descriptor has size
PASSlist descriptor has type
PASSlist descriptor has uploaded
PASSGET /list/<pubkey>?limit=1 returns 200
PASSlimit=1 returns at most 1 result
PASScursor pagination — initial list succeeds
PASSinitial list has entries
PASScursor pagination — second page returns 200
WARNcursor excludes previous entry
Expected false, got true
PASSresults sorted by uploaded date descending — list succeeds
WARNGET /list returns 400 for malformed query params
Expected 400, got 200
HTTP 200 | Body: [{"sha256":"73dddd4952f8a2bc426a1a519dacdc0d4b29e955ac1bcac2549ed836a3cbbcb9","size":67622,"uploaded":1777471183,"type":"image/png","url":"https://blossom.yakihonne.com/73dddd4952f8a2bc426a1a519dacdc0
PASS AUTHdelete test — upload setup succeeds
PASSdelete returns 200 or 204 on success
WARNdeleted blob returns 404 on GET
Expected 404, got 200
HTTP 200
PASSdelete non-existent blob returns 404
PASSdelete without authorization returns 401
PASSdelete requires x tag — upload succeeds
PASSdelete with wrong x tag returns 401 or 403
PASSmultiple x tags — delete blob1 succeeds
PASSmultiple x tags do not delete multiple blobs
nostr.download route96 https://nostr.download
90%
159/176
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
29 pass 5 fail
85%
Test
PASS AUTHupload blob for BUD-01 tests
FAILGET response includes Access-Control-Allow-Origin: *
Expected "*", got null
access-control-allow-origin: (missing)
FAILOPTIONS preflight includes Access-Control-Allow-Headers
Expected non-null value, got null
access-control-allow-headers: (missing)
FAILOPTIONS preflight includes Access-Control-Allow-Methods
Expected non-null value, got null
access-control-allow-methods: (missing)
FAILerror response has 4xx/5xx status
Expected 200 > 399
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
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
FAILAccess-Control-Allow-Origin: * on GET
Expected "*", got null
access-control-allow-origin: (missing)
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSaccept-ranges contains 'bytes'
PASSGET with Range header returns 200 or 206
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
PASSContent-Type defaults to application/octet-stream — upload succeeds
PASSContent-Type defaults to application/octet-stream — GET succeeds
PASSContent-Type header present for octet-stream blob
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
28 pass 1 warn
97%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 201
HTTP 201 | Body: {"url":"https://nostr.download/3e619869473d7b5b6afceb78c8cfe45a74203bcf878bf5e6d409c1996ac160cf.png","sha256":"3e619869473d7b5b6afceb78c8cfe45a74203bcf878bf5e6d409c1996ac160cf","size":69387,"type":"im
PASSdescriptor contains required fields — upload succeeds
PASSdescriptor is valid JSON with required fields
PASSdescriptor has url field
PASSdescriptor has sha256 field
PASSdescriptor has size field
PASSdescriptor has type field
PASS AUTHdescriptor has uploaded field
PASSdescriptor sha256 matches — upload succeeds
PASSdescriptor sha256 matches uploaded blob hash
PASSdescriptor size matches — upload succeeds
PASSdescriptor size is greater than 0
PASSdescriptor type reflects MIME — upload succeeds
PASSdescriptor has type field
PASSdescriptor uploaded is timestamp — upload succeeds
PASSdescriptor uploaded is an integer
PASSdescriptor uploaded > 0
PASSdescriptor url includes extension — upload succeeds
PASSdescriptor URL includes file extension
PASSdescriptor type falls back for unknown — upload succeeds
PASSdescriptor has type field for unknown MIME
PASS AUTHnew blob returns 201 Created
PASSduplicate blob — first upload succeeds
PASSduplicate blob returns 200 OK
PASSserver does not modify blob — upload succeeds
PASSserver does not modify blob — GET succeeds
PASSdownloaded blob hash matches original
PASSX-SHA-256 header is accepted
PASS AUTHupload works without prior HEAD /upload request
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
11 pass 1 warn
92%
Test
PASS AUTHupload blob for BUD-04 tests
PASSmirror endpoint accepts URL in request body
PASSmirrored blob returns blob descriptor
PASSmirror descriptor sha256 matches original
PASSmirror descriptor has url
PASSmirror descriptor has size
PASSmirror descriptor has type
PASSmirror descriptor has uploaded
PASSexisting mirror returns 200 OK
PASSmalformed request body returns 400
WARNmissing url in body returns 400
Expected 400, got 422
HTTP 422 | Body: Failed to deserialize the JSON body into the target type: missing field `url` at line 1 column 2
PASSunreachable URL returns 502 Bad Gateway
BUD-05 OPTIONAL
11 pass 3 warn
79%
Test
PASS AUTHPUT /media accepts binary data and returns 200 or 201
PASSPUT /media returns blob descriptor on success
PASSmedia descriptor has url
PASSmedia descriptor has sha256
PASSmedia descriptor has size
PASSmedia descriptor has type
PASSmedia descriptor has uploaded
PASSPUT /media X-SHA-256 header is accepted
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 201
HTTP 201 | Body: {"url":"https://nostr.download/f5c1c9f3428bde59faf9b977e86b40b10b0960bebbcee767e4c8da9ee1be5e5c.webp","sha256":"f5c1c9f3428bde59faf9b977e86b40b10b0960bebbcee767e4c8da9ee1be5e5c","size":17718,"type":"i
WARNHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
Expected 200, got 204
HTTP 204
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 204
HTTP 204
PASSHEAD /media X-Reason header on error — status is 4xx
PASSX-Reason header on error is human-readable
BUD-06 OPTIONAL
5 pass 5 warn
50%
Test
WARNHEAD /upload accepts headers and returns 200
Expected 200, got 204
HTTP 204
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 204
HTTP 204
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 204
HTTP 204
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 204
HTTP 204
PASSHEAD /upload too large X-Content-Length returns 413
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
WARNpreflight 200 allows subsequent PUT upload
Expected 200, got 204
HTTP 204
PASS AUTHpreflight followed by PUT upload succeeds
BUD-07 OPTIONAL
3 pass
100%
Test
PASS AUTH402 Payment Required — server does not require payment
PASSX-Cashu — server does not require payment
PASSX-Lightning — server does not require payment
BUD-08 OPTIONAL
28 pass
100%
Test
PASS AUTHupload blob for BUD-08 tests
PASSnip94 field is a JSON array when present
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 entry is an array
PASSnip94 entry has length >= 2
PASSnip94 entry key is a string
PASSnip94 contains url tag
PASSnip94 url tag matches descriptor url
PASSnip94 contains x tag matching sha256
PASSnip94 x tag matches sha256
PASSnip94 contains m tag matching MIME type
PASSnip94 m tag matches type
PASSmirror response nip94 test — mirror succeeds
PASSmirror response nip94 field is a JSON array when present
BUD-09 OPTIONAL
4 pass
100%
Test
PASS AUTHPUT /report accepts signed kind:1984 report event
PASS AUTHPUT /report with multiple x tags is accepted
PASS AUTHPUT /report with e/p tags is accepted
PASS AUTHPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
12 pass
100%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
PASS AUTHGET /<sha256> with t=get auth — upload setup succeeds
PASSGET /<sha256> with t=get auth returns 200
PASS AUTHPUT /upload with t=upload auth succeeds
PASSDELETE /<sha256> with t=delete auth succeeds
PASSGET /list/<pubkey> with t=list auth returns 200
PASSPUT /mirror with t=upload auth succeeds
PASS AUTHPUT /media with t=media auth succeeds
PASSDELETE without authorization returns 401
PASSDELETE with wrong x tag returns 401 or 403
BUD-12 OPTIONAL
27 pass
100%
Test
PASS AUTHupload blob for BUD-12 tests
PASSGET /list/<pubkey> returns 200
PASSGET /list/<pubkey> returns JSON array
PASSblob descriptors contain required fields — list succeeds
PASSlist has at least one entry
PASSlist descriptor has url
PASSlist descriptor has sha256
PASSlist descriptor has size
PASSlist descriptor has type
PASSlist descriptor has uploaded
PASSGET /list/<pubkey>?limit=1 returns 200
PASSlimit=1 returns at most 1 result
PASScursor pagination — initial list succeeds
PASSinitial list has entries
PASScursor pagination — second page returns 200
PASScursor excludes previous entry
PASSresults sorted by uploaded date descending — list succeeds
PASSGET /list returns 400 for malformed query params
PASS AUTHdelete test — upload setup succeeds
PASSdelete returns 200 or 204 on success
PASSdeleted blob returns 404 on GET
PASSdelete non-existent blob returns 404
PASSdelete without authorization returns 401
PASSdelete requires x tag — upload succeeds
PASSdelete with wrong x tag returns 401 or 403
PASSmultiple x tags — delete blob1 succeeds
PASSmultiple x tags do not delete multiple blobs
Nostria nostria https://milo.nostria.app
89%
134/151
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
33 pass 2 fail
94%
Test
PASS AUTHupload blob for BUD-01 tests
PASSGET response includes Access-Control-Allow-Origin: *
FAILOPTIONS preflight includes Access-Control-Allow-Headers
Expected non-null value, got null
access-control-allow-headers: (missing)
FAILOPTIONS preflight includes Access-Control-Allow-Methods
Expected non-null value, got null
access-control-allow-methods: (missing)
PASSerror response has 4xx/5xx status
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
PASSAccess-Control-Allow-Origin: * on GET
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSGET with Range header returns 200 or 206
PASS206 response has Content-Range header
PASSContent-Range contains 'bytes'
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
PASSContent-Type defaults to application/octet-stream — upload succeeds
PASSContent-Type defaults to application/octet-stream — GET succeeds
PASSContent-Type header present for octet-stream blob
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
27 pass 2 warn
93%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request
PASSdescriptor contains required fields — upload succeeds
PASSdescriptor is valid JSON with required fields
PASSdescriptor has url field
PASSdescriptor has sha256 field
PASSdescriptor has size field
PASSdescriptor has type field
PASS AUTHdescriptor has uploaded field
PASSdescriptor sha256 matches — upload succeeds
PASSdescriptor sha256 matches uploaded blob hash
PASSdescriptor size matches — upload succeeds
PASSdescriptor size is greater than 0
PASSdescriptor type reflects MIME — upload succeeds
PASSdescriptor has type field
PASSdescriptor uploaded is timestamp — upload succeeds
PASSdescriptor uploaded is an integer
PASSdescriptor uploaded > 0
PASSdescriptor url includes extension — upload succeeds
PASSdescriptor URL includes file extension
PASSdescriptor type falls back for unknown — upload succeeds
PASSdescriptor has type field for unknown MIME
WARN AUTHnew blob returns 201 Created
Expected 201, got 200
HTTP 200 | Body: {"sha256":"c69c224a10e9e16b15b723d0086d06410d1df37fc81777ba4833b20600ccd1ec","size":69135,"uploaded":1777471205,"type":"image/png","url":"https://milo.nostria.app/c69c224a10e9e16b15b723d0086d06410d1df
PASSduplicate blob — first upload succeeds
PASSduplicate blob returns 200 OK
PASSserver does not modify blob — upload succeeds
PASSserver does not modify blob — GET succeeds
PASSdownloaded blob hash matches original
PASSX-SHA-256 header is accepted
PASS AUTHupload works without prior HEAD /upload request
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
11 pass 1 warn
92%
Test
PASS AUTHupload blob for BUD-04 tests
PASSmirror endpoint accepts URL in request body
PASSmirrored blob returns blob descriptor
PASSmirror descriptor sha256 matches original
PASSmirror descriptor has url
PASSmirror descriptor has size
PASSmirror descriptor has type
PASSmirror descriptor has uploaded
PASSexisting mirror returns 200 OK
PASSmalformed request body returns 400
PASSmissing url in body returns 400
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 500
HTTP 500 | X-Reason: Something went wrong | Body: Internal Server Error
BUD-05 OPTIONAL
12 pass 2 warn
86%
Test
PASS AUTHPUT /media accepts binary data and returns 200 or 201
PASSPUT /media returns blob descriptor on success
PASSmedia descriptor has url
PASSmedia descriptor has sha256
PASSmedia descriptor has size
PASSmedia descriptor has type
PASSmedia descriptor has uploaded
PASSPUT /media X-SHA-256 header is accepted
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: Incorrect blob sha256 | Body: Bad Request
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
PASSHEAD /media X-Reason header on error — status is 4xx
PASSX-Reason header on error is human-readable
BUD-06 OPTIONAL
7 pass 3 warn
70%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
PASSHEAD /upload too large X-Content-Length returns 413
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
PASSpreflight 200 allows subsequent PUT upload
PASS AUTHpreflight followed by PUT upload succeeds
BUD-07 OPTIONAL
3 pass
100%
Test
PASS AUTH402 Payment Required — server does not require payment
PASSX-Cashu — server does not require payment
PASSX-Lightning — server does not require payment
BUD-08 OPTIONAL
2 pass
100%
Test
PASS AUTHupload blob for BUD-08 tests
PASSmirror response nip94 test — mirror succeeds
BUD-09 OPTIONAL
1 pass 3 warn
25%
Test
WARNPUT /report accepts signed kind:1984 report event
Expected one of [200, 201, 204], got 405
HTTP 405 | Body: Method Not Allowed
WARNPUT /report with multiple x tags is accepted
Expected one of [200, 201, 204], got 405
HTTP 405 | Body: Method Not Allowed
WARNPUT /report with e/p tags is accepted
Expected one of [200, 201, 204], got 405
HTTP 405 | Body: Method Not Allowed
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
12 pass
100%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
PASS AUTHGET /<sha256> with t=get auth — upload setup succeeds
PASSGET /<sha256> with t=get auth returns 200
PASS AUTHPUT /upload with t=upload auth succeeds
PASSDELETE /<sha256> with t=delete auth succeeds
PASSGET /list/<pubkey> with t=list auth returns 200
PASSPUT /mirror with t=upload auth succeeds
PASS AUTHPUT /media with t=media auth succeeds
PASSDELETE without authorization returns 401
PASSDELETE with wrong x tag returns 401 or 403
BUD-12 OPTIONAL
25 pass 2 warn
93%
Test
PASS AUTHupload blob for BUD-12 tests
PASSGET /list/<pubkey> returns 200
PASSGET /list/<pubkey> returns JSON array
PASSblob descriptors contain required fields — list succeeds
PASSlist has at least one entry
PASSlist descriptor has url
PASSlist descriptor has sha256
PASSlist descriptor has size
PASSlist descriptor has type
PASSlist descriptor has uploaded
PASSGET /list/<pubkey>?limit=1 returns 200
PASSlimit=1 returns at most 1 result
PASScursor pagination — initial list succeeds
PASSinitial list has entries
PASScursor pagination — second page returns 200
WARNcursor excludes previous entry
Expected false, got true
PASSresults sorted by uploaded date descending — list succeeds
WARNGET /list returns 400 for malformed query params
Expected 400, got 200
HTTP 200 | Body: [{"sha256":"2443ae507c6c2cb798207af8c413ff784817043ea0c22a332bbdd4fdad102414","size":68841,"uploaded":1777471217,"type":"image/png","url":"https://milo.nostria.app/2443ae507c6c2cb798207af8c413ff784817
PASS AUTHdelete test — upload setup succeeds
PASSdelete returns 200 or 204 on success
PASSdeleted blob returns 404 on GET
PASSdelete non-existent blob returns 404
PASSdelete without authorization returns 401
PASSdelete requires x tag — upload succeeds
PASSdelete with wrong x tag returns 401 or 403
PASSmultiple x tags — delete blob1 succeeds
PASSmultiple x tags do not delete multiple blobs
NostrMedia nostrmedia https://nostrmedia.com
29%
20/68
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
8 pass 6 fail
57%
Test
FAILupload blob for BUD-01 tests
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
PASSGET response includes Access-Control-Allow-Origin: *
PASSOPTIONS preflight includes Access-Control-Allow-Headers
FAILOPTIONS Allow-Headers includes Authorization
Expected string to contain "authorization"
access-control-allow-headers: *
PASSOPTIONS preflight includes Access-Control-Allow-Methods
PASSOPTIONS Allow-Methods includes GET
PASSOPTIONS Allow-Methods includes HEAD
PASSOPTIONS Allow-Methods includes PUT
FAILOPTIONS Allow-Methods includes DELETE
Expected string to contain "DELETE"
FAILerror response has 4xx/5xx status
Expected 200 > 399
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
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
FAILupload endpoint is accessible at /upload
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
BUD-02 OPTIONAL
0 pass 15 warn
0%
Test
WARNupload without X-SHA-256 returns 200/201 or 400 if required
Expected one of [200, 201, 400], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 403
HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to upload files."}
WARNdescriptor contains required fields — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNdescriptor sha256 matches — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNdescriptor size matches — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNdescriptor type reflects MIME — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNdescriptor uploaded is timestamp — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNdescriptor url includes extension — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNnew blob returns 201 Created
Expected 201, got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNduplicate blob — first upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNduplicate blob returns 200 OK
Expected 200, got 403
HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to upload files."}
WARNserver does not modify blob — upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNX-SHA-256 header is accepted
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNupload works without prior HEAD /upload request
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
1 pass 3 warn
25%
Test
WARNupload blob for BUD-04 tests
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNmalformed request body returns 400
Expected 400, got 500
HTTP 500 | Body: {"status":"error","message":"Unexpected token 'o', \"not json\" is not valid JSON"}
PASSmissing url in body returns 400
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 403
HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to mirror files."}
BUD-05 OPTIONAL
3 pass 6 warn
33%
Test
WARN AUTHPUT /media accepts binary data and returns 200 or 201
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"}
WARNPUT /media returns blob descriptor on success
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"}
WARNPUT /media X-SHA-256 header is accepted
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"}
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 401
HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"}
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200 | X-Reason: Missing auth handler
WARNHEAD /media X-Reason header on error — status is 4xx
Expected 200 > 399
HTTP 200 | X-Reason: Missing auth handler
PASSX-Reason header on error is human-readable
BUD-06 OPTIONAL
3 pass 7 warn
30%
Test
WARNHEAD /upload accepts headers and returns 200
Expected 200, got 401
HTTP 401 | X-Reason: Missing auth handler
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 401
HTTP 401 | X-Reason: Missing auth handler
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 401
HTTP 401 | X-Reason: Missing auth handler
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 401
HTTP 401 | X-Reason: Missing auth handler
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 401
HTTP 401 | X-Reason: Missing auth handler
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
WARNpreflight 200 allows subsequent PUT upload
Expected 200, got 401
HTTP 401 | X-Reason: Missing auth handler
WARNpreflight followed by PUT upload succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
BUD-07 OPTIONAL
0 pass 3 warn
0%
Test
WARN402 Payment Required — server does not require payment
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNX-Cashu — server does not require payment
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNX-Lightning — server does not require payment
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
BUD-08 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-08 tests
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
BUD-09 OPTIONAL
1 pass
100%
Test
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
3 pass 4 warn
43%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
WARNserver accepts standard base64 encoded auth token (not base64url)
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"A paid subscription (Purple, Onyx, or Gold) is required to upload files."}
WARNGET /<sha256> with t=get auth — upload setup succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
WARNPUT /upload with t=upload auth succeeds
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
PASSGET /list/<pubkey> with t=list auth returns 200
WARN AUTHPUT /media with t=media auth succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"status":"error","message":"Invalid or expired authorization event"}
BUD-12 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-12 tests
Expected one of [200, 201, 202], got 403
HTTP 403 | Body: {"status":"error","message":"Authorization required"}
nostrcheck.me nostrcheck https://nostrcheck.me
30%
20/67
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
9 pass 5 fail
64%
Test
FAILupload blob for BUD-01 tests
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
PASSGET response includes Access-Control-Allow-Origin: *
PASSOPTIONS preflight includes Access-Control-Allow-Headers
PASSOPTIONS Allow-Headers includes Authorization
PASSOPTIONS preflight includes Access-Control-Allow-Methods
PASSOPTIONS Allow-Methods includes GET
FAILOPTIONS Allow-Methods includes HEAD
Expected string to contain "HEAD"
PASSOPTIONS Allow-Methods includes PUT
PASSOPTIONS Allow-Methods includes DELETE
PASSerror response has 4xx/5xx status
FAILGET /<sha256> returns 404 for non-existent blob
Expected 404, got 200
HTTP 200 | X-Reason: File not found
PASSHEAD returns 404 for non-existent blob
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
FAILupload endpoint is accessible at /upload
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
BUD-02 OPTIONAL
0 pass 15 warn
0%
Test
WARNupload without X-SHA-256 returns 200/201 or 400 if required
Expected one of [200, 201, 400], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor contains required fields — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor sha256 matches — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor size matches — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor type reflects MIME — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor uploaded is timestamp — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor url includes extension — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARN AUTHnew blob returns 201 Created
Expected 201, got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNduplicate blob — first upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNduplicate blob returns 200 OK
Expected 200, got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNserver does not modify blob — upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNX-SHA-256 header is accepted
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARN AUTHupload works without prior HEAD /upload request
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
1 pass 3 warn
25%
Test
WARNupload blob for BUD-04 tests
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
PASSmalformed request body returns 400
WARNmissing url in body returns 400
Expected 400, got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
BUD-05 OPTIONAL
2 pass 6 warn
25%
Test
WARNPUT /media accepts binary data and returns 200 or 201
Expected one of [200, 201, 202], got 404
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>
WARNPUT /media returns blob descriptor on success
Expected one of [200, 201, 202], got 404
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>
WARNPUT /media X-SHA-256 header is accepted
Expected one of [200, 201, 202], got 404
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>
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 404
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>
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /media X-Reason header on error — status is 4xx
Expected 200 > 399
HTTP 200
BUD-06 OPTIONAL
3 pass 7 warn
30%
Test
WARNHEAD /upload accepts headers and returns 200
Expected 200, got 401
HTTP 401 | X-Reason: Auth header server tag does not include this host
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 401
HTTP 401 | X-Reason: Auth header server tag does not include this host
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 401
HTTP 401 | X-Reason: Auth header server tag does not include this host
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 401
HTTP 401 | X-Reason: Auth header server tag does not include this host
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 401
HTTP 401 | X-Reason: Auth header server tag does not include this host
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
WARNpreflight 200 allows subsequent PUT upload
Expected 200, got 401
HTTP 401 | X-Reason: Auth header server tag does not include this host
WARN AUTHpreflight followed by PUT upload succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
BUD-07 OPTIONAL
0 pass 3 warn
0%
Test
WARN AUTH402 Payment Required — server does not require payment
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNX-Cashu — server does not require payment
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNX-Lightning — server does not require payment
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
BUD-08 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-08 tests
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
BUD-09 OPTIONAL
1 pass
100%
Test
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
3 pass 4 warn
43%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
WARNserver accepts standard base64 encoded auth token (not base64url)
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNGET /<sha256> with t=get auth — upload setup succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
WARNPUT /upload with t=upload auth succeeds
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /media with t=media auth succeeds
Expected one of [200, 201, 202], got 404
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>
BUD-12 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-12 tests
Expected one of [200, 201, 202], got 401
HTTP 401 | Body: {"result":false,"description":"Auth header server tag does not include this host"}
data.haus blossom-server-rs https://blossom.data.haus
86%
121/140
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
41 pass
100%
Test
PASS AUTHupload blob for BUD-01 tests
PASSGET response includes Access-Control-Allow-Origin: *
PASSOPTIONS preflight includes Access-Control-Allow-Headers
PASSOPTIONS Allow-Headers includes Authorization
PASSOPTIONS preflight includes Access-Control-Allow-Methods
PASSOPTIONS Allow-Methods includes GET
PASSOPTIONS Allow-Methods includes HEAD
PASSOPTIONS Allow-Methods includes PUT
PASSOPTIONS Allow-Methods includes DELETE
PASSerror response has 4xx/5xx status
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
PASSAccess-Control-Allow-Origin: * on GET
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSaccept-ranges contains 'bytes'
PASSGET with Range header returns 200 or 206
PASS206 response has Content-Range header
PASSContent-Range contains 'bytes'
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
PASSContent-Type defaults to application/octet-stream — upload succeeds
PASSContent-Type defaults to application/octet-stream — GET succeeds
PASSContent-Type header present for octet-stream blob
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
29 pass
100%
Test
PASSX-SHA-256 mismatch returns 409 Conflict
PASSdescriptor contains required fields — upload succeeds
PASSdescriptor is valid JSON with required fields
PASSdescriptor has url field
PASSdescriptor has sha256 field
PASSdescriptor has size field
PASSdescriptor has type field
PASS AUTHdescriptor has uploaded field
PASSdescriptor sha256 matches — upload succeeds
PASSdescriptor sha256 matches uploaded blob hash
PASSdescriptor size matches — upload succeeds
PASSdescriptor size is greater than 0
PASSdescriptor type reflects MIME — upload succeeds
PASSdescriptor has type field
PASSdescriptor uploaded is timestamp — upload succeeds
PASSdescriptor uploaded is an integer
PASSdescriptor uploaded > 0
PASSdescriptor url includes extension — upload succeeds
PASSdescriptor URL includes file extension
PASSdescriptor type falls back for unknown — upload succeeds
PASSdescriptor has type field for unknown MIME
PASS AUTHnew blob returns 201 Created
PASSduplicate blob — first upload succeeds
PASSduplicate blob returns 200 OK
PASSserver does not modify blob — upload succeeds
PASSserver does not modify blob — GET succeeds
PASSdownloaded blob hash matches original
PASSX-SHA-256 header is accepted
PASS AUTHupload works without prior HEAD /upload request
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
12 pass
100%
Test
PASS AUTHupload blob for BUD-04 tests
PASSmirror endpoint accepts URL in request body
PASSmirrored blob returns blob descriptor
PASSmirror descriptor sha256 matches original
PASSmirror descriptor has url
PASSmirror descriptor has size
PASSmirror descriptor has type
PASSmirror descriptor has uploaded
PASSexisting mirror returns 200 OK
PASSmalformed request body returns 400
PASSmissing url in body returns 400
PASSunreachable URL returns 502 Bad Gateway
BUD-05 OPTIONAL
5 pass 4 warn
56%
Test
WARN AUTHPUT /media accepts binary data and returns 200 or 201
Expected one of [200, 201, 202], got 500
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'
WARNPUT /media returns blob descriptor on success
Expected one of [200, 201, 202], got 500
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'
WARNPUT /media X-SHA-256 header is accepted
Expected one of [200, 201, 202], got 500
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'
PASSPUT /media X-SHA-256 mismatch returns 409 Conflict
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
PASSHEAD /media X-Reason header on error — status is 4xx
PASSX-Reason header on error is human-readable
BUD-06 OPTIONAL
6 pass 4 warn
60%
Test
WARNHEAD /upload accepts headers and returns 200
Expected 200, got 204
HTTP 204
PASSHEAD /upload response has no body
PASSHEAD /upload missing X-Content-Length returns 411
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 204
HTTP 204
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 204
HTTP 204
PASSHEAD /upload too large X-Content-Length returns 413
PASSHEAD /upload X-Reason on error — status is 4xx
PASSX-Reason header on error is human-readable
WARNpreflight 200 allows subsequent PUT upload
Expected 200, got 204
HTTP 204
PASS AUTHpreflight followed by PUT upload succeeds
BUD-07 OPTIONAL
3 pass
100%
Test
PASS AUTH402 Payment Required — server does not require payment
PASSX-Cashu — server does not require payment
PASSX-Lightning — server does not require payment
BUD-08 OPTIONAL
2 pass
100%
Test
PASS AUTHupload blob for BUD-08 tests
PASSmirror response nip94 test — mirror succeeds
BUD-09 OPTIONAL
2 pass 1 warn
67%
Test
PASSPUT /report accepts signed kind:1984 report event
PASSPUT /report with multiple x tags is accepted
WARNPUT /report with invalid blob hash is rejected with 4xx/5xx
Expected 200 > 399
HTTP 200 | Body: {"success":true}
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
10 pass 2 warn
83%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
PASSserver accepts standard base64 encoded auth token (not base64url)
PASS AUTHGET /<sha256> with t=get auth — upload setup succeeds
PASSGET /<sha256> with t=get auth returns 200
PASS AUTHPUT /upload with t=upload auth succeeds
PASSDELETE /<sha256> with t=delete auth succeeds
WARNGET /list/<pubkey> with t=list auth returns 200
Expected 200, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
PASSPUT /mirror with t=upload auth succeeds
WARN AUTHPUT /media with t=media auth succeeds
Expected one of [200, 201, 202], got 500
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'
PASSDELETE without authorization returns 401
PASSDELETE with wrong x tag returns 401 or 403
BUD-12 OPTIONAL
10 pass 6 warn
63%
Test
PASS AUTHupload blob for BUD-12 tests
WARNGET /list/<pubkey> returns 200
Expected 200, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
WARNblob descriptors contain required fields — list succeeds
Expected 200, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
WARNGET /list/<pubkey>?limit=1 returns 200
Expected 200, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
WARNcursor pagination — initial list succeeds
Expected 200, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
WARNresults sorted by uploaded date descending — list succeeds
Expected 200, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
WARNGET /list returns 400 for malformed query params
Expected 400, got 404
HTTP 404 | X-Reason: List endpoint is disabled on this server | Body: List endpoint is disabled on this server
PASS AUTHdelete test — upload setup succeeds
PASSdelete returns 200 or 204 on success
PASSdeleted blob returns 404 on GET
PASSdelete non-existent blob returns 404
PASSdelete without authorization returns 401
PASSdelete requires x tag — upload succeeds
PASSdelete with wrong x tag returns 401 or 403
PASSmultiple x tags — delete blob1 succeeds
PASSmultiple x tags do not delete multiple blobs
Sovbit sovbit https://cdn.sovbit.host
49%
42/86
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
30 pass 4 fail
88%
Test
PASS AUTHupload blob for BUD-01 tests
PASSGET response includes Access-Control-Allow-Origin: *
FAILOPTIONS preflight includes Access-Control-Allow-Headers
Expected non-null value, got null
access-control-allow-headers: (missing)
PASSOPTIONS preflight includes Access-Control-Allow-Methods
PASSOPTIONS Allow-Methods includes GET
PASSOPTIONS Allow-Methods includes HEAD
PASSOPTIONS Allow-Methods includes PUT
PASSOPTIONS Allow-Methods includes DELETE
PASSerror response has 4xx/5xx status
FAILGET /<sha256> returns 404 for non-existent blob
Expected 404, got 200
HTTP 200
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
PASSAccess-Control-Allow-Origin: * on GET
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
FAILContent-Length header present
Expected non-null value, got null
content-length: (missing)
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSGET with Range header returns 200 or 206
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 400
HTTP 400 | Body: {"status":"error","message":"Empty file"}
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
0 pass 14 warn
0%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 202
HTTP 202 | Body: {"status":"success","message":"","url":"https://cdn.sovbit.host/61c6a808a1fdfdb0e691899fa99ec36fb003c9810b2897c442751c91af75f42c.png","sha256":"61c6a808a1fdfdb0e691899fa99ec36fb003c9810b2897c442751c91
WARNdescriptor contains required fields — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNdescriptor sha256 matches — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNdescriptor size matches — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNdescriptor type reflects MIME — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNdescriptor uploaded is timestamp — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNdescriptor url includes extension — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNnew blob returns 201 Created
Expected 201, got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNduplicate blob — first upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNduplicate blob returns 200 OK
Expected 200, got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNserver does not modify blob — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNX-SHA-256 header is accepted
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNupload works without prior HEAD /upload request
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
0 pass 4 warn
0%
Test
WARNupload blob for BUD-04 tests
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNmalformed request body returns 400
Expected 400, got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNmissing url in body returns 400
Expected 400, got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
BUD-05 OPTIONAL
2 pass 6 warn
25%
Test
WARNPUT /media accepts binary data and returns 200 or 201
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNPUT /media returns blob descriptor on success
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNPUT /media X-SHA-256 header is accepted
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
Expected 200, got 404
HTTP 404
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 404
HTTP 404
PASSHEAD /media X-Reason header on error — status is 4xx
BUD-06 OPTIONAL
5 pass 5 warn
50%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 400
HTTP 400 | X-Reason: Missing size, MIME type or SHA-256
PASSHEAD /upload missing X-SHA-256 returns 400
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 200
HTTP 200 | X-Reason: Upload allowed
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 200
HTTP 200 | X-Reason: Upload allowed
WARNHEAD /upload X-Reason on error — status is 4xx
Expected 200 > 399
HTTP 200 | X-Reason: Upload allowed
PASSX-Reason header on error is human-readable
PASSpreflight 200 allows subsequent PUT upload
WARNpreflight followed by PUT upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
BUD-07 OPTIONAL
0 pass 3 warn
0%
Test
WARN402 Payment Required — server does not require payment
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNX-Cashu — server does not require payment
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNX-Lightning — server does not require payment
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
BUD-08 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-08 tests
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
BUD-09 OPTIONAL
1 pass
100%
Test
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
3 pass 4 warn
43%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
WARNserver accepts standard base64 encoded auth token (not base64url)
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNGET /<sha256> with t=get auth — upload setup succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
WARNPUT /upload with t=upload auth succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /media with t=media auth succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
BUD-12 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-12 tests
Expected one of [200, 201, 202], got 429
HTTP 429 | Body: {"status":"error","message":"Rate limit exceeded. Try again in a few minutes."}
Azzamo azzamo https://blossom.azzamo.media
52%
47/90
BUD-00
1 pass
100%
Test
PASSserver is reachable and responds to HTTP requests
BUD-01
37 pass 2 fail
95%
Test
PASS AUTHupload blob for BUD-01 tests
PASSGET response includes Access-Control-Allow-Origin: *
PASSOPTIONS preflight includes Access-Control-Allow-Headers
PASSOPTIONS Allow-Headers includes Authorization
PASSOPTIONS preflight includes Access-Control-Allow-Methods
PASSOPTIONS Allow-Methods includes GET
PASSOPTIONS Allow-Methods includes HEAD
PASSOPTIONS Allow-Methods includes PUT
PASSOPTIONS Allow-Methods includes DELETE
FAILerror response has 4xx/5xx status
Expected 200 > 399
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
PASSGET /<sha256> returns 404 for non-existent blob
PASSHEAD returns 404 for non-existent blob
PASSGET /<sha256> returns 200 OK
PASSGET /<sha256> response body matches uploaded blob — GET succeeds
PASSdownloaded blob hash matches upload
PASSGET /<sha256> Content-Type header is set
PASSContent-Type header present
PASSGET /<sha256>.png accepts optional file extension
PASSCORS header present on GET response
PASSAccess-Control-Allow-Origin: * on GET
PASSredirect URLs contain same sha256 hash (no redirect)
PASSHEAD /<sha256> returns 200 OK for existing blob
PASSHEAD returns same Content-Type as GET
PASSHEAD Content-Type matches GET Content-Type
PASSHEAD returns Content-Length header
PASSContent-Length header present
PASSContent-Length > 0
PASSHEAD does not return blob body
PASSHEAD response body is empty
PASSHEAD accepts optional file extension in URL
PASSHEAD response includes accept-ranges: bytes
PASSaccept-ranges contains 'bytes'
PASSGET with Range header returns 200 or 206
PASS206 response has Content-Range header
PASSContent-Range contains 'bytes'
PASSSunset header test — GET succeeds
PASSblob retrieval endpoint is accessible at /<sha256>
FAILContent-Type defaults to application/octet-stream — upload succeeds
Expected one of [200, 201, 202], got 415
HTTP 415 | X-Reason: MIME type application/octet-stream is not supported | Body: {"message":"MIME type application/octet-stream is not supported"}
PASS AUTHupload endpoint is accessible at /upload
BUD-02 OPTIONAL
0 pass 14 warn
0%
Test
WARNX-SHA-256 mismatch returns 409 Conflict
Expected 409, got 400
HTTP 400 | X-Reason: SHA-256 hash does not match expected value | Body: {"message":"SHA-256 hash does not match expected value"}
WARNdescriptor contains required fields — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNdescriptor sha256 matches — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNdescriptor size matches — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNdescriptor type reflects MIME — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNdescriptor uploaded is timestamp — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNdescriptor url includes extension — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNdescriptor type falls back for unknown — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNnew blob returns 201 Created
Expected 201, got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNduplicate blob — first upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNduplicate blob returns 200 OK
Expected 200, got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNserver does not modify blob — upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNX-SHA-256 header is accepted
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNupload works without prior HEAD /upload request
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
BUD-03 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-03 defines kind:10063 event format and client URL parsing — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-04 OPTIONAL
0 pass 4 warn
0%
Test
WARNupload blob for BUD-04 tests
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNmalformed request body returns 400
Expected 400, got 500
HTTP 500 | X-Reason: Something went wrong | Body: {"error":"INTERNAL_ERROR","message":"Unexpected token 'n', \"not json\" is not valid JSON"}
WARNmissing url in body returns 400
Expected 400, got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNunreachable URL returns 502 Bad Gateway
Expected 502, got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
BUD-05 OPTIONAL
2 pass 6 warn
25%
Test
WARNPUT /media accepts binary data and returns 200 or 201
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNPUT /media returns blob descriptor on success
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNPUT /media X-SHA-256 header is accepted
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNPUT /media X-SHA-256 mismatch returns 409 Conflict
Expected 409, got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
PASSHEAD /media uses X-SHA-256, X-Content-Type, X-Content-Length headers
PASSHEAD /media returns status code without response body
WARNHEAD /media missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /media X-Reason header on error — status is 4xx
Expected 200 > 399
HTTP 200
BUD-06 OPTIONAL
3 pass 6 warn
33%
Test
PASSHEAD /upload accepts headers and returns 200
PASSHEAD /upload response has no body
WARNHEAD /upload missing X-Content-Length returns 411
Expected 411, got 200
HTTP 200
WARNHEAD /upload missing X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
WARNHEAD /upload malformed X-SHA-256 returns 400
Expected 400, got 200
HTTP 200
WARNHEAD /upload too large X-Content-Length returns 413
Expected 413, got 200
HTTP 200
WARNHEAD /upload X-Reason on error — status is 4xx
Expected 200 > 399
HTTP 200
PASSpreflight 200 allows subsequent PUT upload
WARNpreflight followed by PUT upload succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
BUD-07 OPTIONAL
0 pass 3 warn
0%
Test
WARN402 Payment Required — server does not require payment
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNX-Cashu — server does not require payment
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNX-Lightning — server does not require payment
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
BUD-08 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-08 tests
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
BUD-09 OPTIONAL
1 pass
100%
Test
PASSPUT /report with invalid blob hash is rejected with 4xx/5xx
BUD-10 OPTIONAL
0 pass 1 skip
0%
Test
SKIPBUD-10 defines the blossom: URI scheme for client-side blob discovery — no server endpoints defined
client-side only, no server endpoints
client-side only, no server endpoints
BUD-11 OPTIONAL
3 pass 4 warn
43%
Test
PASSexpired token is rejected by server
PASSwrong t tag verb is rejected
WARNserver accepts standard base64 encoded auth token (not base64url)
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNGET /<sha256> with t=get auth — upload setup succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
WARNPUT /upload with t=upload auth succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
PASSGET /list/<pubkey> with t=list auth returns 200
WARNPUT /media with t=media auth succeeds
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}
BUD-12 OPTIONAL
0 pass 1 warn
0%
Test
WARNupload blob for BUD-12 tests
Expected one of [200, 201, 202], got 429
HTTP 429 | X-Reason: Rate limit exceeded | Body: {"error":"Too many requests","message":"Rate limit exceeded. Please try again later."}