2026-08-18 16:35:20 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Integration test for Mailship
|
|
|
|
|
#
|
|
|
|
|
# Starts the server, runs through the full E2E flow, cleans up.
|
|
|
|
|
# Exits 0 on success, 1 on failure.
|
|
|
|
|
#
|
|
|
|
|
# Usage: ./test/integration.sh [--server-only]
|
|
|
|
|
# --server-only: just start the server (for manual testing)
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
|
PORT="${PORT:-4741}"
|
|
|
|
|
BASE_URL="http://localhost:$PORT"
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
NC='\033[0m'
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=== Cleaning up ==="
|
|
|
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
|
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
|
|
|
rm -rf "$PROJECT_DIR/test-data"
|
|
|
|
|
echo "Done."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trap cleanup EXIT
|
|
|
|
|
|
|
|
|
|
# Ensure we have a fresh build
|
|
|
|
|
cd "$PROJECT_DIR"
|
|
|
|
|
if [ ! -d "dist" ]; then
|
|
|
|
|
echo "Building project..."
|
|
|
|
|
npx tsc
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Generate secrets for the test: one for the server, one for the client
|
|
|
|
|
SERVER_SECRET="$(openssl rand -hex 32)"
|
|
|
|
|
CLIENT_SECRET="$(openssl rand -hex 32)"
|
|
|
|
|
|
|
|
|
|
# Derive the client pubkey so we can look up subscriptions later
|
|
|
|
|
CLIENT_PUBKEY=$(node -e "
|
|
|
|
|
import {Nip01Signer} from '@welshman/signer';
|
|
|
|
|
const s = Nip01Signer.fromSecret('$CLIENT_SECRET');
|
|
|
|
|
s.getPubkey().then(p => console.log(p));
|
|
|
|
|
")
|
|
|
|
|
|
|
|
|
|
echo "Client pubkey: $CLIENT_PUBKEY"
|
|
|
|
|
|
|
|
|
|
# Helper to build a NIP-98 auth header
|
|
|
|
|
nip98_auth() {
|
|
|
|
|
local url="$1" method="$2" body="${3:-}"
|
|
|
|
|
if [ -n "$body" ]; then
|
|
|
|
|
node "$PROJECT_DIR/script/nip98-auth-header.mjs" "$CLIENT_SECRET" "$url" "$method" "$body"
|
|
|
|
|
else
|
|
|
|
|
node "$PROJECT_DIR/script/nip98-auth-header.mjs" "$CLIENT_SECRET" "$url" "$method"
|
|
|
|
|
fi
|
|
|
|
|
}
|
2026-08-18 16:35:20 +00:00
|
|
|
|
|
|
|
|
# Export env vars for the server
|
2026-09-14 17:04:26 +00:00
|
|
|
export MAILSHIP_SECRET="$SERVER_SECRET"
|
2026-08-18 16:35:20 +00:00
|
|
|
export MAILSHIP_NAME="Mailship Test"
|
|
|
|
|
export MAILSHIP_URL="$BASE_URL"
|
|
|
|
|
export BASE_URL="$BASE_URL"
|
Align README, docker-compose, .env.template with SMTP mailer
Replace all Postmark references (POSTMARK_API_KEY, POSTMARK_SENDER_ADDRESS)
with SMTP configuration (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD,
SMTP_FROM) across documentation, deployment config, template, and test.
- README.md: architecture diagram (Postmark → SMTP) and configuration table
- docker-compose.yml: POSTMARK_* env vars replaced with SMTP_* required vars
- .env.template: POSTMARK_* entries replaced with SMTP_* entries
- test/integration.sh: POSTMARK_* test exports replaced with SMTP_* test values
2026-09-10 15:30:50 +00:00
|
|
|
export SMTP_HOST="localhost"
|
|
|
|
|
export SMTP_PORT="587"
|
|
|
|
|
export SMTP_USER="test@test.com"
|
|
|
|
|
export SMTP_PASSWORD="test"
|
|
|
|
|
export SMTP_FROM="test@test.com"
|
2026-08-18 16:35:20 +00:00
|
|
|
export DEFAULT_RELAYS="wss://relay.damus.io"
|
|
|
|
|
export INDEXER_RELAYS="wss://purplepag.es"
|
|
|
|
|
export SEARCH_RELAYS="wss://relay.nostr.band"
|
|
|
|
|
export PORT="$PORT"
|
2026-09-14 17:07:16 +00:00
|
|
|
export CORS_ORIGIN="$BASE_URL"
|
2026-08-18 16:35:20 +00:00
|
|
|
export DATA_DIR="$PROJECT_DIR/test-data"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$DATA_DIR"
|
|
|
|
|
DB_PATH="$DATA_DIR/mailship.db"
|
|
|
|
|
|
|
|
|
|
echo "=== Starting server on port $PORT ==="
|
|
|
|
|
node dist/index.js &
|
|
|
|
|
SERVER_PID=$!
|
|
|
|
|
sleep 3
|
|
|
|
|
|
|
|
|
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
|
|
|
echo -e "${RED}Server failed to start${NC}"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Server PID: $SERVER_PID"
|
|
|
|
|
|
|
|
|
|
if [ "${1:-}" = "--server-only" ]; then
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Server running at $BASE_URL"
|
|
|
|
|
echo "DB at $DB_PATH"
|
|
|
|
|
echo "Press Ctrl+C to stop."
|
|
|
|
|
wait "$SERVER_PID"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========================================="
|
|
|
|
|
echo " INTEGRATION TESTS"
|
|
|
|
|
echo "========================================="
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
pass() {
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
echo -e " ${GREEN}✓${NC} $1"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fail() {
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
echo -e " ${RED}✗${NC} $1"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Helper: check that a JSON field equals expected value
|
|
|
|
|
check_field() {
|
|
|
|
|
local label="$1"
|
|
|
|
|
local json="$2"
|
|
|
|
|
local field="$3"
|
|
|
|
|
local expected="$4"
|
|
|
|
|
local actual
|
|
|
|
|
actual=$(echo "$json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$field',''))" 2>/dev/null)
|
|
|
|
|
if [ "$actual" = "$expected" ]; then
|
|
|
|
|
pass "$label"
|
|
|
|
|
else
|
|
|
|
|
fail "$label (expected $field=$expected, got $actual)"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Test 1: Health check
|
|
|
|
|
echo "1. Health check"
|
|
|
|
|
HEALTH=$(curl -s "$BASE_URL/")
|
|
|
|
|
check_field "Root endpoint returns Mailship" "$HEALTH" "name" "Mailship"
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 2: Register subscription with NIP-98 auth
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "2. Register subscription (NIP-98 auth)"
|
|
|
|
|
AUTH_PUT=$(nip98_auth "$BASE_URL/subscription/email" PUT '{"email":"test@example.com","frequency":"daily"}')
|
2026-09-03 15:47:21 +00:00
|
|
|
REG=$(curl -s "$BASE_URL/subscription/email" -X PUT -H "Content-Type: application/json" \
|
2026-09-14 17:04:26 +00:00
|
|
|
-H "Authorization: $AUTH_PUT" \
|
|
|
|
|
-d '{"email":"test@example.com","frequency":"daily"}')
|
2026-08-18 16:35:20 +00:00
|
|
|
|
|
|
|
|
KEY=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('key',''))" 2>/dev/null)
|
|
|
|
|
CALLBACK=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('callback',''))" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ -n "$KEY" ] && [ -n "$CALLBACK" ]; then
|
|
|
|
|
pass "Registration returns key and callback"
|
|
|
|
|
else
|
|
|
|
|
fail "Registration missing key or callback (got: $REG)"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 3: PUT without auth returns 401
|
|
|
|
|
echo ""
|
|
|
|
|
echo "3. PUT without auth returns 401"
|
|
|
|
|
NO_AUTH=$(curl -s "$BASE_URL/subscription/email" -X PUT -H "Content-Type: application/json" \
|
|
|
|
|
-d '{"email":"test@example.com","frequency":"daily"}')
|
|
|
|
|
check_field "No-auth PUT returns 401" "$NO_AUTH" "error" "NIP-98 authorization required"
|
|
|
|
|
|
|
|
|
|
# Test 4: GET /subscription/email with auth
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "4. GET subscription with auth"
|
|
|
|
|
AUTH_GET=$(nip98_auth "$BASE_URL/subscription/email" GET)
|
|
|
|
|
GET_RESP=$(curl -s "$BASE_URL/subscription/email" \
|
|
|
|
|
-H "Authorization: $AUTH_GET")
|
|
|
|
|
check_field "GET returns our email" "$GET_RESP" "email" "test@example.com"
|
|
|
|
|
check_field "GET returns frequency" "$GET_RESP" "frequency" "daily"
|
|
|
|
|
|
|
|
|
|
# Test 5: GET without auth returns 401
|
|
|
|
|
echo ""
|
|
|
|
|
echo "5. GET without auth returns 401"
|
|
|
|
|
GET_NO_AUTH=$(curl -s "$BASE_URL/subscription/email")
|
|
|
|
|
check_field "No-auth GET returns 401" "$GET_NO_AUTH" "error" "NIP-98 authorization required"
|
|
|
|
|
|
|
|
|
|
# Test 6: Confirm subscription
|
|
|
|
|
echo ""
|
|
|
|
|
echo "6. Confirm subscription"
|
2026-08-18 16:35:20 +00:00
|
|
|
CONFIRM=$(curl -s "$BASE_URL/confirm?token=$KEY" 2>&1)
|
|
|
|
|
if echo "$CONFIRM" | grep -qi "success"; then
|
|
|
|
|
pass "Confirmation page shows success"
|
|
|
|
|
else
|
|
|
|
|
fail "Confirmation page doesn't show success"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 7: Check SQLite state
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "7. Database state"
|
|
|
|
|
CONFIRMED=$(sqlite3 "$DB_PATH" "SELECT confirmed_at FROM subscriptions WHERE pubkey='$CLIENT_PUBKEY';" 2>/dev/null)
|
2026-08-18 16:35:20 +00:00
|
|
|
if [ -n "$CONFIRMED" ] && [ "$CONFIRMED" -gt 0 ]; then
|
|
|
|
|
pass "Subscription confirmed in DB"
|
|
|
|
|
else
|
|
|
|
|
fail "Subscription not confirmed in DB"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 8: Push event to notify endpoint
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "8. Push event via notify"
|
|
|
|
|
SUB_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM subscriptions WHERE pubkey='$CLIENT_PUBKEY';" 2>/dev/null)
|
2026-08-18 16:35:20 +00:00
|
|
|
|
|
|
|
|
# Fetch a real event from a relay
|
|
|
|
|
EVENT_ID=$(nak req -k 1 -l 1 wss://relay.primal.net 2>/dev/null | head -1 | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ -z "$EVENT_ID" ]; then
|
|
|
|
|
# Try another relay
|
|
|
|
|
EVENT_ID=$(nak req -k 1 -l 1 wss://nos.lol 2>/dev/null | head -1 | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])" 2>/dev/null)
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$EVENT_ID" ]; then
|
|
|
|
|
fail "Could not fetch event from relay (network issue?)"
|
|
|
|
|
else
|
|
|
|
|
NOTIFY=$(curl -s "$BASE_URL/notify/$SUB_ID" -X POST -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"id\":\"$EVENT_ID\",\"relay\":\"wss://relay.primal.net\"}")
|
|
|
|
|
check_field "Event stored in DB" "$NOTIFY" "stored" "True"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 9: Dedup
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "9. Dedup"
|
2026-08-18 16:35:20 +00:00
|
|
|
if [ -n "$EVENT_ID" ]; then
|
|
|
|
|
DEDUP=$(curl -s "$BASE_URL/notify/$SUB_ID" -X POST -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"id\":\"$EVENT_ID\",\"relay\":\"wss://relay.primal.net\"}")
|
|
|
|
|
check_field "Duplicate event rejected" "$DEDUP" "stored" "False"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 10: 404 for nonexistent subscription
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "10. 404 for nonexistent subscription"
|
2026-08-18 16:35:20 +00:00
|
|
|
NOT_FOUND=$(curl -s "$BASE_URL/notify/nonexistent-id" -X POST -H "Content-Type: application/json" \
|
|
|
|
|
-d '{"id":"abc","relay":"wss://relay.primal.net"}')
|
|
|
|
|
check_field "Nonexistent subscription returns 404" "$NOT_FOUND" "error" "Subscription not found"
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 11: Unsubscribe
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "11. Unsubscribe"
|
2026-08-18 16:35:20 +00:00
|
|
|
UNSUB=$(curl -s "$BASE_URL/unsubscribe?token=$KEY")
|
|
|
|
|
if echo "$UNSUB" | grep -qi "unsubscribed\|success"; then
|
|
|
|
|
pass "Unsubscribe page renders"
|
|
|
|
|
else
|
|
|
|
|
fail "Unsubscribe page didn't render"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 12: Notify after unsubscribe returns 404
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "12. No push after unsubscribe"
|
2026-08-18 16:35:20 +00:00
|
|
|
if [ -n "$EVENT_ID" ]; then
|
|
|
|
|
AFTER_UNSUB=$(curl -s "$BASE_URL/notify/$SUB_ID" -X POST -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"id\":\"$EVENT_ID\",\"relay\":\"wss://relay.primal.net\"}")
|
|
|
|
|
check_field "Push after unsubscribe returns 404" "$AFTER_UNSUB" "error" "Subscription not active"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 13: Delete subscription with auth
|
2026-08-18 16:35:20 +00:00
|
|
|
echo ""
|
2026-09-14 17:04:26 +00:00
|
|
|
echo "13. Delete subscription with NIP-98 auth"
|
|
|
|
|
AUTH_DEL=$(nip98_auth "$BASE_URL/subscription/$KEY" DELETE)
|
|
|
|
|
DELETE=$(curl -s "$BASE_URL/subscription/$KEY" -X DELETE -H "Authorization: $AUTH_DEL")
|
2026-08-18 16:35:20 +00:00
|
|
|
check_field "Delete returns ok" "$DELETE" "ok" "True"
|
|
|
|
|
|
2026-09-14 17:04:26 +00:00
|
|
|
# Test 14: Delete without auth returns 401
|
|
|
|
|
echo ""
|
|
|
|
|
echo "14. Delete without auth returns 401"
|
|
|
|
|
DEL_NO_AUTH=$(curl -s "$BASE_URL/subscription/$KEY" -X DELETE)
|
|
|
|
|
check_field "No-auth DELETE returns 401" "$DEL_NO_AUTH" "error" "NIP-98 authorization required"
|
|
|
|
|
|
2026-08-18 16:35:20 +00:00
|
|
|
# Summary
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========================================="
|
|
|
|
|
echo " RESULTS: $PASS passed, $FAIL failed"
|
|
|
|
|
echo "========================================="
|
|
|
|
|
|
|
|
|
|
if [ "$FAIL" -gt 0 ]; then
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
exit 0
|