Add integration test script, fix root handler, add DATA_DIR support
- test/integration.sh: runs full E2E flow against fresh server - pnpm test and pnpm test:server scripts added - Root endpoint handles missing web UI gracefully (returns JSON) - database.ts reads DATA_DIR env var for test isolation - .gitignore updated for test artifacts
This commit is contained in:
parent
25645e1d0e
commit
f220301154
5 changed files with 245 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,4 +7,5 @@ anchor.db
|
|||
dist
|
||||
emails/*.html
|
||||
mailship.db
|
||||
test-mailship.db
|
||||
dist/
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
"check": "tsc --noEmit && eslint src",
|
||||
"format": "prettier --write \"src/**/*.{ts,js,json,html}\"",
|
||||
"start": "node dist/index.js",
|
||||
"run-alert": "node dist/run.js"
|
||||
"run-alert": "node dist/run.js",
|
||||
"test": "bash test/integration.sh",
|
||||
"test:server": "bash test/integration.sh --server-only"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { instrument } from 'succinct-async'
|
|||
import { now } from '@welshman/lib'
|
||||
import type { Subscription } from './alert.js'
|
||||
|
||||
const db = new sqlite3.Database('mailship.db')
|
||||
const DATA_DIR = process.env.DATA_DIR || '.'
|
||||
const db = new sqlite3.Database(DATA_DIR + '/mailship.db')
|
||||
|
||||
type Param = number | string | boolean
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,22 @@ const addRoute = (method: 'get' | 'post' | 'delete', path: string, handler: Hand
|
|||
}
|
||||
|
||||
addRoute('get', '/', async (req: Request, res: Response) => {
|
||||
res.send(await render('../web/dist/index.html'))
|
||||
try {
|
||||
const {existsSync} = await import('fs')
|
||||
const webIndex = new URL('../web/dist/index.html', import.meta.url)
|
||||
if (existsSync(webIndex)) {
|
||||
return res.send(await render('../web/dist/index.html'))
|
||||
}
|
||||
} catch {
|
||||
// Fall through to JSON
|
||||
}
|
||||
|
||||
res.json({
|
||||
name: 'Mailship',
|
||||
description: 'Email notification server for Nostr',
|
||||
pubkey: await appSigner.getPubkey(),
|
||||
software: 'https://gitea.coracle.social/mplorentz/mailship',
|
||||
})
|
||||
})
|
||||
|
||||
// Subscribe to email digests
|
||||
|
|
|
|||
223
test/integration.sh
Executable file
223
test/integration.sh
Executable file
|
|
@ -0,0 +1,223 @@
|
|||
#!/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
|
||||
|
||||
# Generate a random secret for the test
|
||||
SECRET="$(openssl rand -hex 32)"
|
||||
|
||||
# Export env vars for the server
|
||||
export MAILSHIP_SECRET="$SECRET"
|
||||
export MAILSHIP_NAME="Mailship Test"
|
||||
export MAILSHIP_URL="$BASE_URL"
|
||||
export BASE_URL="$BASE_URL"
|
||||
export POSTMARK_API_KEY="test"
|
||||
export POSTMARK_SENDER_ADDRESS="test@test.com"
|
||||
export DEFAULT_RELAYS="wss://relay.damus.io"
|
||||
export INDEXER_RELAYS="wss://purplepag.es"
|
||||
export SEARCH_RELAYS="wss://relay.nostr.band"
|
||||
export PORT="$PORT"
|
||||
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"
|
||||
|
||||
# Test 2: Register subscription
|
||||
echo ""
|
||||
echo "2. Register subscription"
|
||||
REG=$(curl -s "$BASE_URL/subscription/email" -X POST -H "Content-Type: application/json" \
|
||||
-d '{"email":"test@example.com","frequency":"daily","pubkey":"abc123"}')
|
||||
|
||||
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
|
||||
|
||||
# Test 3: Confirm subscription
|
||||
echo ""
|
||||
echo "3. Confirm subscription"
|
||||
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
|
||||
|
||||
# Test 4: Check SQLite state
|
||||
echo ""
|
||||
echo "4. Database state"
|
||||
CONFIRMED=$(sqlite3 "$DB_PATH" "SELECT confirmed_at FROM subscriptions WHERE email='test@example.com';" 2>/dev/null)
|
||||
if [ -n "$CONFIRMED" ] && [ "$CONFIRMED" -gt 0 ]; then
|
||||
pass "Subscription confirmed in DB"
|
||||
else
|
||||
fail "Subscription not confirmed in DB"
|
||||
fi
|
||||
|
||||
# Test 5: Push event to notify endpoint
|
||||
echo ""
|
||||
echo "5. Push event via notify"
|
||||
SUB_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM subscriptions WHERE email='test@example.com';" 2>/dev/null)
|
||||
|
||||
# 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
|
||||
|
||||
# Test 6: Dedup
|
||||
echo ""
|
||||
echo "6. Dedup"
|
||||
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
|
||||
|
||||
# Test 7: 404 for nonexistent subscription
|
||||
echo ""
|
||||
echo "7. 404 for nonexistent subscription"
|
||||
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"
|
||||
|
||||
# Test 8: Unsubscribe
|
||||
echo ""
|
||||
echo "8. Unsubscribe"
|
||||
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
|
||||
|
||||
# Test 9: Notify after unsubscribe returns 404
|
||||
echo ""
|
||||
echo "9. No push after unsubscribe"
|
||||
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
|
||||
|
||||
# Test 10: Delete subscription
|
||||
echo ""
|
||||
echo "10. Delete subscription"
|
||||
DELETE=$(curl -s "$BASE_URL/subscription/$KEY" -X DELETE 2>&1)
|
||||
check_field "Delete returns ok" "$DELETE" "ok" "True"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " RESULTS: $PASS passed, $FAIL failed"
|
||||
echo "========================================="
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Loading…
Reference in a new issue