9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
379 lines
14 KiB
Bash
379 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
# test-video-upload.sh — Live validation of the Blossom video upload flow.
|
|
#
|
|
# Prerequisites:
|
|
# - Relay running at $RELAY_URL (default: http://localhost:3000)
|
|
# - Dev mode (BUZZ_REQUIRE_AUTH_TOKEN=false) or valid API token
|
|
# - ffmpeg, nak, curl, jq, shasum on PATH
|
|
#
|
|
# Usage:
|
|
# ./scripts/test-video-upload.sh # run all tests
|
|
# RELAY_URL=http://host:3000 ./scripts/... # custom relay URL
|
|
# NSEC=nsec1... ./scripts/... # use existing key
|
|
|
|
set -euo pipefail
|
|
|
|
RELAY_URL="${RELAY_URL:-http://localhost:3000}"
|
|
TMPDIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
pass() { echo " ✅ $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo " ❌ $1"; FAIL=$((FAIL + 1)); }
|
|
|
|
# ── Dependencies ───────────────────────────────────────────────────────────────
|
|
|
|
for cmd in ffmpeg nak curl jq shasum; do
|
|
command -v "$cmd" >/dev/null 2>&1 || { echo "Missing: $cmd"; exit 1; }
|
|
done
|
|
|
|
# ── Key generation ─────────────────────────────────────────────────────────────
|
|
|
|
if [ -z "${NSEC:-}" ]; then
|
|
NSEC="$(nak key generate)"
|
|
echo "Generated key: $(echo "$NSEC" | nak key public)"
|
|
fi
|
|
NPUB="$(echo "$NSEC" | nak key public)"
|
|
echo "Using pubkey: $NPUB"
|
|
echo "Relay: $RELAY_URL"
|
|
echo ""
|
|
|
|
# ── Generate test MP4 ─────────────────────────────────────────────────────────
|
|
# Minimal 1-second H.264 video with moov at front (faststart).
|
|
|
|
TEST_MP4="$TMPDIR/test.mp4"
|
|
ffmpeg -y -f lavfi -i "color=c=blue:s=320x240:d=1" \
|
|
-c:v libx264 -profile:v baseline -pix_fmt yuv420p \
|
|
-movflags +faststart \
|
|
"$TEST_MP4" 2>/dev/null
|
|
|
|
FILE_SIZE=$(wc -c < "$TEST_MP4" | tr -d ' ')
|
|
SHA256=$(shasum -a 256 "$TEST_MP4" | cut -d' ' -f1)
|
|
echo "Test MP4: ${FILE_SIZE} bytes, sha256=${SHA256:0:16}..."
|
|
echo ""
|
|
|
|
# ── Helper: build Blossom auth header ──────────────────────────────────────────
|
|
# Creates a kind:24242 event with t=upload, x=<sha256>, expiration=+5min.
|
|
|
|
blossom_auth() {
|
|
local sha256="$1"
|
|
local now exp auth_event auth_b64
|
|
|
|
now=$(date +%s)
|
|
exp=$((now + 300))
|
|
|
|
auth_event=$(nak event \
|
|
--sec "$NSEC" \
|
|
-k 24242 \
|
|
-c "Upload test video" \
|
|
-t t=upload \
|
|
-t "x=$sha256" \
|
|
-t "expiration=$exp" \
|
|
2>/dev/null)
|
|
|
|
auth_b64=$(echo -n "$auth_event" | base64 | tr -d '\n')
|
|
echo "Nostr $auth_b64"
|
|
}
|
|
|
|
# ── Test 1: Upload MP4 ────────────────────────────────────────────────────────
|
|
|
|
echo "Test 1: Upload MP4 via PUT /media/upload"
|
|
AUTH="$(blossom_auth "$SHA256")"
|
|
|
|
UPLOAD_RESP=$(curl -s -w "\n%{http_code}" \
|
|
-X PUT "$RELAY_URL/media/upload" \
|
|
-H "Authorization: $AUTH" \
|
|
-H "Content-Type: video/mp4" \
|
|
-H "X-SHA-256: $SHA256" \
|
|
--data-binary "@$TEST_MP4")
|
|
|
|
UPLOAD_HTTP=$(echo "$UPLOAD_RESP" | tail -1)
|
|
UPLOAD_BODY=$(echo "$UPLOAD_RESP" | sed '$d')
|
|
|
|
if [ "$UPLOAD_HTTP" = "200" ]; then
|
|
pass "Upload returned 200"
|
|
BLOB_URL=$(echo "$UPLOAD_BODY" | jq -r '.url // empty')
|
|
if [ -n "$BLOB_URL" ]; then
|
|
pass "Response contains url: ${BLOB_URL:0:60}..."
|
|
else
|
|
fail "Response missing url field"
|
|
fi
|
|
# Check duration field present
|
|
DURATION=$(echo "$UPLOAD_BODY" | jq -r '.duration // empty')
|
|
if [ -n "$DURATION" ]; then
|
|
pass "Response contains duration: ${DURATION}s"
|
|
else
|
|
fail "Response missing duration field"
|
|
fi
|
|
else
|
|
fail "Upload returned $UPLOAD_HTTP (expected 200)"
|
|
echo " Body: $UPLOAD_BODY"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 2: GET full blob ─────────────────────────────────────────────────────
|
|
|
|
echo "Test 2: GET /media/${SHA256}.mp4 (full download)"
|
|
GET_RESP=$(curl -s -o "$TMPDIR/downloaded.mp4" -w "%{http_code}" \
|
|
"$RELAY_URL/media/${SHA256}.mp4")
|
|
|
|
if [ "$GET_RESP" = "200" ]; then
|
|
pass "GET returned 200"
|
|
DL_SIZE=$(wc -c < "$TMPDIR/downloaded.mp4" | tr -d ' ')
|
|
if [ "$DL_SIZE" = "$FILE_SIZE" ]; then
|
|
pass "Downloaded size matches ($DL_SIZE bytes)"
|
|
else
|
|
fail "Size mismatch: expected $FILE_SIZE, got $DL_SIZE"
|
|
fi
|
|
else
|
|
fail "GET returned $GET_RESP (expected 200)"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 3: HEAD with Accept-Ranges ──────────────────────────────────────────
|
|
|
|
echo "Test 3: HEAD /media/${SHA256}.mp4 (Accept-Ranges)"
|
|
HEAD_RESP=$(curl -s -I "$RELAY_URL/media/${SHA256}.mp4")
|
|
HEAD_HTTP=$(echo "$HEAD_RESP" | head -1 | grep -o '[0-9]\{3\}')
|
|
ACCEPT_RANGES=$(echo "$HEAD_RESP" | grep -i "accept-ranges" | tr -d '\r')
|
|
|
|
if [ "$HEAD_HTTP" = "200" ]; then
|
|
pass "HEAD returned 200"
|
|
else
|
|
fail "HEAD returned $HEAD_HTTP (expected 200)"
|
|
fi
|
|
|
|
if echo "$ACCEPT_RANGES" | grep -qi "bytes"; then
|
|
pass "Accept-Ranges: bytes present"
|
|
else
|
|
fail "Accept-Ranges header missing or wrong: '$ACCEPT_RANGES'"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 4: Range GET (206 Partial Content) ──────────────────────────────────
|
|
|
|
echo "Test 4: Range GET bytes=0-499 (206 Partial Content)"
|
|
RANGE_RESP=$(curl -s -o "$TMPDIR/range.bin" -w "%{http_code}" \
|
|
-H "Range: bytes=0-499" \
|
|
"$RELAY_URL/media/${SHA256}.mp4")
|
|
|
|
if [ "$RANGE_RESP" = "206" ]; then
|
|
pass "Range GET returned 206"
|
|
RANGE_SIZE=$(wc -c < "$TMPDIR/range.bin" | tr -d ' ')
|
|
if [ "$RANGE_SIZE" = "500" ]; then
|
|
pass "Received exactly 500 bytes"
|
|
else
|
|
fail "Expected 500 bytes, got $RANGE_SIZE"
|
|
fi
|
|
else
|
|
fail "Range GET returned $RANGE_RESP (expected 206)"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 5: Range GET past EOF (416) ─────────────────────────────────────────
|
|
|
|
echo "Test 5: Range GET bytes=999999999- (416 Range Not Satisfiable)"
|
|
RANGE416_RESP=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-H "Range: bytes=999999999-" \
|
|
"$RELAY_URL/media/${SHA256}.mp4")
|
|
|
|
if [ "$RANGE416_RESP" = "416" ]; then
|
|
pass "Past-EOF range returned 416"
|
|
else
|
|
fail "Past-EOF range returned $RANGE416_RESP (expected 416)"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 6: Content-Type spoofing rejection ──────────────────────────────────
|
|
# Send video/mp4 Content-Type but with a PNG body — should be rejected.
|
|
|
|
echo "Test 6: Content-Type spoofing (video/mp4 header, PNG body)"
|
|
PNG_FILE="$TMPDIR/fake.png"
|
|
# Minimal valid PNG (1x1 red pixel)
|
|
printf '\x89PNG\r\n\x1a\n' > "$PNG_FILE"
|
|
dd if=/dev/zero bs=100 count=1 >> "$PNG_FILE" 2>/dev/null
|
|
|
|
PNG_SHA=$(shasum -a 256 "$PNG_FILE" | cut -d' ' -f1)
|
|
SPOOF_AUTH="$(blossom_auth "$PNG_SHA")"
|
|
|
|
SPOOF_RESP=$(curl -s -w "\n%{http_code}" \
|
|
-X PUT "$RELAY_URL/media/upload" \
|
|
-H "Authorization: $SPOOF_AUTH" \
|
|
-H "Content-Type: video/mp4" \
|
|
-H "X-SHA-256: $PNG_SHA" \
|
|
--data-binary "@$PNG_FILE")
|
|
|
|
SPOOF_HTTP=$(echo "$SPOOF_RESP" | tail -1)
|
|
|
|
if [ "$SPOOF_HTTP" = "415" ] || [ "$SPOOF_HTTP" = "400" ]; then
|
|
pass "Spoofed upload rejected with $SPOOF_HTTP"
|
|
else
|
|
fail "Spoofed upload returned $SPOOF_HTTP (expected 400 or 415)"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 7: Idempotent re-upload ─────────────────────────────────────────────
|
|
|
|
echo "Test 7: Idempotent re-upload (same file, same hash)"
|
|
REUP_AUTH="$(blossom_auth "$SHA256")"
|
|
REUP_RESP=$(curl -s -w "\n%{http_code}" \
|
|
-X PUT "$RELAY_URL/media/upload" \
|
|
-H "Authorization: $REUP_AUTH" \
|
|
-H "Content-Type: video/mp4" \
|
|
-H "X-SHA-256: $SHA256" \
|
|
--data-binary "@$TEST_MP4")
|
|
|
|
REUP_HTTP=$(echo "$REUP_RESP" | tail -1)
|
|
if [ "$REUP_HTTP" = "200" ]; then
|
|
pass "Re-upload returned 200 (idempotent)"
|
|
else
|
|
fail "Re-upload returned $REUP_HTTP (expected 200)"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 8: Poster frame upload + imeta validation ───────────────────────────
|
|
# Upload a JPEG poster frame, then verify the server accepts an imeta tag
|
|
# that links the video and poster via the NIP-71 `image` field.
|
|
|
|
echo "Test 8: Upload poster frame (JPEG image)"
|
|
POSTER_JPG="$TMPDIR/poster.jpg"
|
|
ffmpeg -y -ss 0.5 -i "$TEST_MP4" -vframes 1 -vf "scale=640:-2" -q:v 2 \
|
|
"$POSTER_JPG" 2>/dev/null
|
|
|
|
if [ ! -s "$POSTER_JPG" ]; then
|
|
# Fallback: first frame (video may be too short for 0.5s seek)
|
|
ffmpeg -y -i "$TEST_MP4" -vframes 1 -vf "scale=640:-2" -q:v 2 \
|
|
"$POSTER_JPG" 2>/dev/null
|
|
fi
|
|
|
|
POSTER_SIZE=$(wc -c < "$POSTER_JPG" | tr -d ' ')
|
|
POSTER_SHA=$(shasum -a 256 "$POSTER_JPG" | cut -d' ' -f1)
|
|
echo " Poster: ${POSTER_SIZE} bytes, sha256=${POSTER_SHA:0:16}..."
|
|
|
|
POSTER_AUTH="$(blossom_auth "$POSTER_SHA")"
|
|
POSTER_RESP=$(curl -s -w "\n%{http_code}" \
|
|
-X PUT "$RELAY_URL/media/upload" \
|
|
-H "Authorization: $POSTER_AUTH" \
|
|
-H "Content-Type: image/jpeg" \
|
|
-H "X-SHA-256: $POSTER_SHA" \
|
|
--data-binary "@$POSTER_JPG")
|
|
|
|
POSTER_HTTP=$(echo "$POSTER_RESP" | tail -1)
|
|
POSTER_BODY=$(echo "$POSTER_RESP" | sed '$d')
|
|
|
|
if [ "$POSTER_HTTP" = "200" ]; then
|
|
pass "Poster upload returned 200"
|
|
POSTER_URL=$(echo "$POSTER_BODY" | jq -r '.url // empty')
|
|
if [ -n "$POSTER_URL" ]; then
|
|
pass "Poster has url: ${POSTER_URL:0:60}..."
|
|
else
|
|
fail "Poster response missing url"
|
|
fi
|
|
# Poster should have dim but NOT duration
|
|
POSTER_DIM=$(echo "$POSTER_BODY" | jq -r '.dim // empty')
|
|
POSTER_DUR=$(echo "$POSTER_BODY" | jq -r '.duration // empty')
|
|
if [ -n "$POSTER_DIM" ]; then
|
|
pass "Poster has dim: $POSTER_DIM"
|
|
else
|
|
fail "Poster missing dim"
|
|
fi
|
|
if [ -z "$POSTER_DUR" ]; then
|
|
pass "Poster correctly omits duration"
|
|
else
|
|
fail "Poster should not have duration, got: $POSTER_DUR"
|
|
fi
|
|
else
|
|
fail "Poster upload returned $POSTER_HTTP (expected 200)"
|
|
echo " Body: $POSTER_BODY"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 9: GET poster frame ─────────────────────────────────────────────────
|
|
|
|
echo "Test 9: GET poster frame"
|
|
POSTER_GET_RESP=$(curl -s -o "$TMPDIR/poster_dl.jpg" -w "%{http_code}" \
|
|
"$RELAY_URL/media/${POSTER_SHA}.jpg")
|
|
|
|
if [ "$POSTER_GET_RESP" = "200" ]; then
|
|
pass "GET poster returned 200"
|
|
POSTER_DL_SIZE=$(wc -c < "$TMPDIR/poster_dl.jpg" | tr -d ' ')
|
|
if [ "$POSTER_DL_SIZE" = "$POSTER_SIZE" ]; then
|
|
pass "Poster download size matches ($POSTER_DL_SIZE bytes)"
|
|
else
|
|
fail "Poster size mismatch: expected $POSTER_SIZE, got $POSTER_DL_SIZE"
|
|
fi
|
|
else
|
|
fail "GET poster returned $POSTER_GET_RESP (expected 200)"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 10: Video + poster blobs coexist and are independently retrievable ──
|
|
# The server links video and poster purely through the imeta tag at message
|
|
# send time. Here we verify the prerequisite: both blobs exist, have correct
|
|
# Content-Types, and are independently addressable.
|
|
|
|
echo "Test 10: Video + poster blobs coexist"
|
|
VIDEO_HEAD=$(curl -s -o /dev/null -w "%{http_code}" -I "$RELAY_URL/media/${SHA256}.mp4")
|
|
POSTER_HEAD=$(curl -s -o /dev/null -w "%{http_code}" -I "$RELAY_URL/media/${POSTER_SHA}.jpg")
|
|
|
|
if [ "$VIDEO_HEAD" = "200" ] && [ "$POSTER_HEAD" = "200" ]; then
|
|
pass "Both video and poster blobs exist (HEAD 200)"
|
|
else
|
|
fail "Blob existence check failed: video=$VIDEO_HEAD poster=$POSTER_HEAD"
|
|
fi
|
|
|
|
# Verify poster is an image (not video) by checking Content-Type
|
|
POSTER_CT=$(curl -s -I "$RELAY_URL/media/${POSTER_SHA}.jpg" | grep -i "content-type" | tr -d '\r' | awk '{print $2}')
|
|
if echo "$POSTER_CT" | grep -qi "image/jpeg"; then
|
|
pass "Poster Content-Type is image/jpeg"
|
|
else
|
|
fail "Poster Content-Type should be image/jpeg, got: $POSTER_CT"
|
|
fi
|
|
|
|
# Verify video and poster have different hashes (independent blobs)
|
|
if [ "$SHA256" != "$POSTER_SHA" ]; then
|
|
pass "Video and poster have distinct content hashes"
|
|
else
|
|
fail "Video and poster hashes should differ"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Test 11: Poster sidecar has correct metadata ─────────────────────────────
|
|
# The server writes a JSON sidecar for every uploaded blob. Verify the poster
|
|
# sidecar exists and has image MIME type (the server's verify_imeta_blobs
|
|
# checks this at message send time).
|
|
|
|
echo "Test 11: Poster sidecar metadata"
|
|
# The bare-hash GET resolves via sidecar — if it returns 200 with image/jpeg
|
|
# content-type, the sidecar is correctly configured.
|
|
POSTER_BARE_RESP=$(curl -s -D "$TMPDIR/poster_bare_headers.txt" -o /dev/null -w "%{http_code}" \
|
|
"$RELAY_URL/media/${POSTER_SHA}")
|
|
if [ "$POSTER_BARE_RESP" = "200" ]; then
|
|
pass "Poster bare-hash GET resolves via sidecar (200)"
|
|
else
|
|
fail "Poster bare-hash GET returned $POSTER_BARE_RESP (expected 200)"
|
|
fi
|
|
|
|
# Verify Content-Type on the bare-hash response
|
|
POSTER_BARE_CT=$(grep -i "content-type" "$TMPDIR/poster_bare_headers.txt" | tr -d '\r' | awk '{print $2}')
|
|
if echo "$POSTER_BARE_CT" | grep -qi "image/jpeg"; then
|
|
pass "Poster bare-hash Content-Type is image/jpeg"
|
|
else
|
|
fail "Poster bare-hash Content-Type should be image/jpeg, got: $POSTER_BARE_CT"
|
|
fi
|
|
|
|
# Note: Full imeta image validation (accept/reject at message send time) is
|
|
# covered by Rust unit tests: test_imeta_image_poster_frame_accepted,
|
|
# test_imeta_image_video_url_rejected, test_imeta_image_thumbnail_url_rejected.
|
|
echo ""
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
|
|
echo "════════════════════════════════════════"
|
|
echo " Results: $PASS passed, $FAIL failed"
|
|
echo "════════════════════════════════════════"
|
|
|
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|