* feat(ap): add support for shared inboxes to reduce outbound load * feat(db): refactor ap followers db into followers repository * fix(ap): use the updated activity library to pull out the shared inbox endpoint * chore(deps): point at updated build of owncast/activity * fix(ap): typeless endpoints * feat(test): update ActivityPub test to support shared inboxes * chore(test): remove unused variable * fix: feedback from review. Guard against SSRF/non-HTTPS/local and handle transaction errors
944 lines
30 KiB
Bash
Executable File
944 lines
30 KiB
Bash
Executable File
#!/bin/bash
|
|
# shellcheck disable=SC2317 # cleanup() is invoked via trap, not direct call
|
|
|
|
# ActivityPub Federation Test using snac2
|
|
#
|
|
# This test:
|
|
# 1. Builds/installs snac2
|
|
# 2. Creates 100 test users in snac2
|
|
# 3. Starts a local HTTPS reverse proxy
|
|
# 4. Starts snac2 server (HTTP internally, HTTPS via proxy)
|
|
# 5. Starts Owncast (HTTP internally, HTTPS via proxy)
|
|
# 6. Has all 100 snac2 users follow Owncast
|
|
# 7. Sends a message from Owncast
|
|
# 8. Verifies message delivery to snac2 inboxes
|
|
#
|
|
# Requirements:
|
|
# - openssl (for generating certs)
|
|
# - curl
|
|
# - node (for the HTTPS proxy)
|
|
# - Go (for building Owncast)
|
|
# - C compiler (for building snac2)
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
|
|
# Configuration
|
|
USER_COUNT="${USER_COUNT:-100}"
|
|
FOLLOW_DELAY="${FOLLOW_DELAY:-0.1}" # Delay between follow requests (seconds)
|
|
CI="${CI:-false}" # Skip interactive prompts for CI environments
|
|
PROXY_PORT="${PROXY_PORT:-8443}"
|
|
SNAC_PORT="${SNAC_PORT:-9080}"
|
|
SNAC_HOSTNAME="snac.local"
|
|
OWNCAST_PORT="${OWNCAST_PORT:-8080}"
|
|
OWNCAST_HOSTNAME="owncast.local"
|
|
ADMIN_USER="admin"
|
|
ADMIN_PASS="abc123"
|
|
FEDERATION_USERNAME="streamer"
|
|
CLEAR_SHARED_INBOX_PERCENT="${CLEAR_SHARED_INBOX_PERCENT:-30}" # Percentage of followers to clear shared_inbox (0 = none)
|
|
|
|
# URLs (HTTPS via proxy)
|
|
SNAC_URL="https://${SNAC_HOSTNAME}:${PROXY_PORT}"
|
|
OWNCAST_URL="https://${OWNCAST_HOSTNAME}:${PROXY_PORT}"
|
|
|
|
# Directories
|
|
TEMP_DIR=""
|
|
SNAC_DATA_DIR=""
|
|
SNAC_BIN=""
|
|
OWNCAST_DB=""
|
|
|
|
# PIDs and state
|
|
SNAC_PID=""
|
|
OWNCAST_PID=""
|
|
PROXY_PID=""
|
|
TEST_STREAM_PID=""
|
|
SNAC_USERNAMES=()
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
log_test() { echo -e "${CYAN}[TEST]${NC} $1"; }
|
|
|
|
kill_leftover_processes() {
|
|
# Kill any leftover test processes from previous runs
|
|
local killed=false
|
|
|
|
# Kill snac2 instances running from /tmp (test instances only)
|
|
if pkill -f "snac httpd /tmp" 2>/dev/null; then
|
|
killed=true
|
|
fi
|
|
|
|
# Kill any local-proxy.js instances
|
|
if pkill -f "local-proxy.js" 2>/dev/null; then
|
|
killed=true
|
|
fi
|
|
|
|
# Kill anything on our test ports
|
|
local proxy_pid
|
|
proxy_pid=$(lsof -ti :"${PROXY_PORT}" 2>/dev/null) || true
|
|
if [[ -n "${proxy_pid}" ]]; then
|
|
kill "${proxy_pid}" 2>/dev/null || true
|
|
killed=true
|
|
fi
|
|
|
|
local snac_pid
|
|
snac_pid=$(lsof -ti :"${SNAC_PORT}" 2>/dev/null) || true
|
|
if [[ -n "${snac_pid}" ]]; then
|
|
kill "${snac_pid}" 2>/dev/null || true
|
|
killed=true
|
|
fi
|
|
|
|
if [[ "${killed}" == "true" ]]; then
|
|
log_info "Killed leftover processes from previous run"
|
|
sleep 1
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
log_info "Cleaning up..."
|
|
|
|
if [[ -n "${PROXY_PID}" ]] && kill -0 "${PROXY_PID}" 2>/dev/null; then
|
|
kill "${PROXY_PID}" 2>/dev/null || true
|
|
wait "${PROXY_PID}" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ -n "${OWNCAST_PID}" ]] && kill -0 "${OWNCAST_PID}" 2>/dev/null; then
|
|
kill "${OWNCAST_PID}" 2>/dev/null || true
|
|
wait "${OWNCAST_PID}" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ -n "${SNAC_PID}" ]] && kill -0 "${SNAC_PID}" 2>/dev/null; then
|
|
kill "${SNAC_PID}" 2>/dev/null || true
|
|
wait "${SNAC_PID}" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ -n "${TEST_STREAM_PID}" ]] && kill -0 "${TEST_STREAM_PID}" 2>/dev/null; then
|
|
kill "${TEST_STREAM_PID}" 2>/dev/null || true
|
|
wait "${TEST_STREAM_PID}" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ -n "${TEMP_DIR}" ]] && [[ -d "${TEMP_DIR}" ]]; then
|
|
rm -rf "${TEMP_DIR}"
|
|
fi
|
|
|
|
log_info "Cleanup complete."
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
setup_temp_dir() {
|
|
TEMP_DIR=$(mktemp -d)
|
|
SNAC_DATA_DIR="${TEMP_DIR}/snac-data"
|
|
OWNCAST_DB="${TEMP_DIR}/owncast.db"
|
|
|
|
log_info "Temp directory: ${TEMP_DIR}"
|
|
}
|
|
|
|
check_hosts_entry() {
|
|
# Check if hosts entries exist
|
|
if ! grep -q "${OWNCAST_HOSTNAME}" /etc/hosts 2>/dev/null || ! grep -q "${SNAC_HOSTNAME}" /etc/hosts 2>/dev/null; then
|
|
log_warn "Required /etc/hosts entries not found."
|
|
log_warn "Please add the following to /etc/hosts:"
|
|
echo ""
|
|
echo " 127.0.0.1 ${OWNCAST_HOSTNAME} ${SNAC_HOSTNAME}"
|
|
echo ""
|
|
log_warn "You may need to run: sudo sh -c 'echo \"127.0.0.1 ${OWNCAST_HOSTNAME} ${SNAC_HOSTNAME}\" >> /etc/hosts'"
|
|
exit 1
|
|
fi
|
|
log_info "Hosts entries verified"
|
|
}
|
|
|
|
install_snac2() {
|
|
log_info "Setting up snac2..."
|
|
|
|
# Check if snac2 is already installed
|
|
if command -v snac &> /dev/null; then
|
|
SNAC_BIN=$(command -v snac)
|
|
log_info "Using system snac2: ${SNAC_BIN}"
|
|
return
|
|
fi
|
|
|
|
# Clone and build snac2
|
|
local snac_src="${TEMP_DIR}/snac2-src"
|
|
log_info "Cloning snac2..."
|
|
git clone --depth 1 https://codeberg.org/grunfink/snac2.git "${snac_src}" 2>/dev/null
|
|
|
|
log_info "Building snac2..."
|
|
pushd "${snac_src}" > /dev/null
|
|
make
|
|
SNAC_BIN="${snac_src}/snac"
|
|
popd > /dev/null
|
|
|
|
log_info "snac2 built: ${SNAC_BIN}"
|
|
}
|
|
|
|
check_certs() {
|
|
# Use pre-generated mkcert certificates from the script directory
|
|
CERT_DIR="${SCRIPT_DIR}/certs"
|
|
|
|
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
|
|
log_error "Certificates not found in ${CERT_DIR}"
|
|
log_error "Please run the one-time setup. See README.md for instructions:"
|
|
log_error " mkcert -install"
|
|
log_error " mkcert -cert-file certs/cert.pem -key-file certs/key.pem owncast.local snac.local localhost 127.0.0.1"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Using certificates from ${CERT_DIR}"
|
|
}
|
|
|
|
init_snac2() {
|
|
log_info "Initializing snac2..."
|
|
|
|
# snac2 advertises URLs via the proxy (HTTPS on PROXY_PORT)
|
|
local snac_host_port="${SNAC_HOSTNAME}:${PROXY_PORT}"
|
|
|
|
# Use snac init with piped input: address, port, hostname, prefix, admin email
|
|
printf "127.0.0.1\n%s\n%s\n\ntest@test.local\n" "${SNAC_PORT}" "${snac_host_port}" | \
|
|
"${SNAC_BIN}" init "${SNAC_DATA_DIR}" > /dev/null 2>&1
|
|
|
|
if [[ ! -f "${SNAC_DATA_DIR}/server.json" ]]; then
|
|
log_error "snac2 init failed - server.json not created"
|
|
return 1
|
|
fi
|
|
|
|
log_info "snac2 initialized"
|
|
}
|
|
|
|
enable_snac_shared_inbox() {
|
|
log_info "Enabling shared_inboxes in snac2 configuration..."
|
|
|
|
local server_json="${SNAC_DATA_DIR}/server.json"
|
|
|
|
if [[ ! -f "${server_json}" ]]; then
|
|
log_error "server.json not found at ${server_json}"
|
|
return 1
|
|
fi
|
|
|
|
# Use jq if available, otherwise use Python as a fallback
|
|
if command -v jq &> /dev/null; then
|
|
local tmp_file
|
|
tmp_file=$(mktemp)
|
|
jq '. + {"shared_inboxes": true}' "${server_json}" > "${tmp_file}" && mv "${tmp_file}" "${server_json}"
|
|
elif command -v python3 &> /dev/null; then
|
|
python3 -c "
|
|
import json
|
|
with open('${server_json}', 'r') as f:
|
|
data = json.load(f)
|
|
data['shared_inboxes'] = True
|
|
with open('${server_json}', 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
"
|
|
else
|
|
log_error "Neither jq nor python3 available to modify server.json"
|
|
return 1
|
|
fi
|
|
|
|
# Verify the setting was applied
|
|
if grep -q '"shared_inboxes"' "${server_json}"; then
|
|
log_info "shared_inboxes enabled in snac2 configuration"
|
|
else
|
|
log_error "Failed to enable shared_inboxes"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
create_snac_users() {
|
|
log_info "Creating ${USER_COUNT} users in snac2 using snac adduser..."
|
|
|
|
# Generate a unique prefix for this test run to avoid conflicts
|
|
local run_id
|
|
run_id=$(date +%s%N | sha256sum | head -c 8)
|
|
log_info "Test run ID: ${run_id}"
|
|
|
|
# Store usernames for later use in follow requests
|
|
SNAC_USERNAMES=()
|
|
|
|
local created=0
|
|
local failed=0
|
|
|
|
for i in $(seq 1 "${USER_COUNT}"); do
|
|
local username="test${run_id}u${i}"
|
|
local displayname="Test User ${i}"
|
|
|
|
# Use snac adduser with piped input (username, display name)
|
|
if printf "%s\n%s\n" "${username}" "${displayname}" | "${SNAC_BIN}" adduser "${SNAC_DATA_DIR}" > /dev/null 2>&1; then
|
|
SNAC_USERNAMES+=("${username}")
|
|
created=$((created + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
|
|
if [[ $((created + failed)) -gt 0 ]] && [[ $(((created + failed) % 20)) -eq 0 ]]; then
|
|
log_info "Created $((created + failed))/${USER_COUNT} users (successful: ${created})..."
|
|
fi
|
|
done
|
|
|
|
log_info "Created ${created} users in snac2 (${failed} failed)"
|
|
}
|
|
|
|
start_proxy() {
|
|
log_info "Starting HTTPS reverse proxy (Caddy)..."
|
|
|
|
# Check if caddy is installed
|
|
if ! command -v caddy &> /dev/null; then
|
|
log_error "Caddy is not installed. Please install it: https://caddyserver.com/docs/install"
|
|
return 1
|
|
fi
|
|
|
|
# Set environment variables for Caddyfile
|
|
export PROXY_PORT="${PROXY_PORT}"
|
|
export OWNCAST_PORT="${OWNCAST_PORT}"
|
|
export SNAC_PORT="${SNAC_PORT}"
|
|
export CERT_FILE="${CERT_DIR}/cert.pem"
|
|
export KEY_FILE="${CERT_DIR}/key.pem"
|
|
|
|
# Redirect caddy output to a log file to reduce test noise
|
|
local caddy_log="${TEMP_DIR}/caddy.log"
|
|
caddy run --config "${SCRIPT_DIR}/Caddyfile" --adapter caddyfile > "${caddy_log}" 2>&1 &
|
|
PROXY_PID=$!
|
|
|
|
log_info "Caddy started with PID ${PROXY_PID}"
|
|
sleep 2
|
|
|
|
# Verify proxy is running
|
|
if ! kill -0 "${PROXY_PID}" 2>/dev/null; then
|
|
log_error "Caddy failed to start"
|
|
return 1
|
|
fi
|
|
|
|
# Verify proxy is accepting connections
|
|
local max_attempts=10
|
|
local attempt=0
|
|
while [[ ${attempt} -lt ${max_attempts} ]]; do
|
|
if curl -sk "https://127.0.0.1:${PROXY_PORT}/" > /dev/null 2>&1; then
|
|
log_info "Caddy proxy is ready"
|
|
return 0
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
sleep 1
|
|
done
|
|
|
|
log_error "Caddy proxy did not become ready"
|
|
return 1
|
|
}
|
|
|
|
start_snac2() {
|
|
log_info "Starting snac2 server..."
|
|
|
|
# mkcert certificates are trusted system-wide, no special env vars needed
|
|
# Redirect snac2 output to a log file to reduce test noise
|
|
local snac_log="${TEMP_DIR}/snac2.log"
|
|
DEBUG=0 "${SNAC_BIN}" httpd "${SNAC_DATA_DIR}" > "${snac_log}" 2>&1 &
|
|
SNAC_PID=$!
|
|
log_info "snac2 logs available at: ${snac_log}"
|
|
|
|
log_info "snac2 started with PID ${SNAC_PID}"
|
|
|
|
# Wait for snac2 to be ready
|
|
local max_attempts=30
|
|
local attempt=0
|
|
|
|
while [[ ${attempt} -lt ${max_attempts} ]]; do
|
|
if curl -s "http://127.0.0.1:${SNAC_PORT}/" > /dev/null 2>&1; then
|
|
log_info "snac2 is ready"
|
|
return 0
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
sleep 1
|
|
done
|
|
|
|
log_error "snac2 did not become ready"
|
|
return 1
|
|
}
|
|
|
|
verify_snac_shared_inbox() {
|
|
log_info "Verifying snac2 actors have sharedInbox endpoint..."
|
|
|
|
# Get the first test user to check their actor object
|
|
if [[ ${#SNAC_USERNAMES[@]} -eq 0 ]]; then
|
|
log_warn "No snac2 users created yet, skipping sharedInbox verification"
|
|
return 0
|
|
fi
|
|
|
|
local test_user="${SNAC_USERNAMES[0]}"
|
|
local actor_url="${SNAC_URL}/${test_user}"
|
|
|
|
# Fetch the actor object and check for sharedInbox
|
|
local actor_response
|
|
actor_response=$(curl -s --max-time 10 -H "Accept: application/activity+json" "${actor_url}" 2>&1)
|
|
|
|
if echo "${actor_response}" | grep -q '"sharedInbox"'; then
|
|
log_info "sharedInbox endpoint found in snac2 actor objects"
|
|
return 0
|
|
else
|
|
log_error "sharedInbox endpoint NOT found in snac2 actor objects"
|
|
log_error "Actor response: ${actor_response}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
build_owncast() {
|
|
log_info "Building Owncast..."
|
|
|
|
pushd "${REPO_ROOT}" > /dev/null
|
|
CGO_ENABLED=1 go build -o owncast main.go
|
|
popd > /dev/null
|
|
|
|
log_info "Owncast built"
|
|
}
|
|
|
|
start_owncast() {
|
|
log_info "Starting Owncast..."
|
|
|
|
# Start Owncast with test environment variables and debug flags
|
|
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
|
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
|
"${REPO_ROOT}/owncast" -database "${OWNCAST_DB}" &
|
|
OWNCAST_PID=$!
|
|
|
|
log_info "Owncast started with PID ${OWNCAST_PID}"
|
|
|
|
# Wait for Owncast to be ready
|
|
local max_attempts=30
|
|
local attempt=0
|
|
|
|
while [[ ${attempt} -lt ${max_attempts} ]]; do
|
|
if curl -s "http://localhost:${OWNCAST_PORT}/api/status" > /dev/null 2>&1; then
|
|
log_info "Owncast is ready"
|
|
return 0
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
sleep 1
|
|
done
|
|
|
|
log_error "Owncast did not become ready"
|
|
return 1
|
|
}
|
|
|
|
configure_owncast() {
|
|
log_info "Configuring Owncast with URL: ${OWNCAST_URL}"
|
|
|
|
local base_url="http://localhost:${OWNCAST_PORT}"
|
|
local auth
|
|
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
|
|
|
|
# Set server URL
|
|
curl -s -X POST "${base_url}/api/admin/config/serverurl" \
|
|
-H "Authorization: Basic ${auth}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"value\": \"${OWNCAST_URL}\"}" > /dev/null
|
|
|
|
# Set federation username
|
|
curl -s -X POST "${base_url}/api/admin/config/federation/username" \
|
|
-H "Authorization: Basic ${auth}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"value\": \"${FEDERATION_USERNAME}\"}" > /dev/null
|
|
|
|
# Enable federation
|
|
curl -s -X POST "${base_url}/api/admin/config/federation/enable" \
|
|
-H "Authorization: Basic ${auth}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"value": true}' > /dev/null
|
|
|
|
# Disable private mode (auto-accept follows)
|
|
curl -s -X POST "${base_url}/api/admin/config/federation/private" \
|
|
-H "Authorization: Basic ${auth}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"value": false}' > /dev/null
|
|
|
|
log_info "Owncast configured"
|
|
}
|
|
|
|
prompt_for_test_stream() {
|
|
# Skip interactive prompts in CI mode
|
|
if [[ "${CI}" == "true" ]]; then
|
|
log_info "CI mode - skipping test stream prompt"
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
echo "----------------------------------------"
|
|
read -p "Would you like to start a test live stream? [y/N] " -n 1 -r
|
|
echo ""
|
|
|
|
if [[ ${REPLY} =~ ^[Yy]$ ]]; then
|
|
log_info "Starting test stream..."
|
|
"${REPO_ROOT}/test/ocTestStream.sh" &
|
|
TEST_STREAM_PID=$!
|
|
|
|
echo ""
|
|
log_info "Test stream started with PID ${TEST_STREAM_PID}"
|
|
log_info "You can view the stream at: http://localhost:${OWNCAST_PORT}"
|
|
echo ""
|
|
read -p "Press Enter when you are watching the stream and ready to continue..." -r
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
send_follow_requests() {
|
|
log_info "Sending follow requests from snac2 users..."
|
|
|
|
local owncast_actor="${OWNCAST_URL}/federation/user/${FEDERATION_USERNAME}"
|
|
log_info "Following actor: ${owncast_actor}"
|
|
|
|
# First verify the actor URL is accessible via curl
|
|
log_info "Verifying actor URL is accessible..."
|
|
local curl_test
|
|
curl_test=$(curl -s --max-time 5 -H "Accept: application/activity+json" "${owncast_actor}" 2>&1)
|
|
if echo "${curl_test}" | grep -q '"type"'; then
|
|
log_info "Actor URL is accessible via curl"
|
|
else
|
|
log_error "Actor URL not accessible: ${curl_test}"
|
|
fi
|
|
|
|
local successful=0
|
|
local failed=0
|
|
local total=${#SNAC_USERNAMES[@]}
|
|
|
|
for username in "${SNAC_USERNAMES[@]}"; do
|
|
# Use snac follow command and capture output
|
|
# mkcert certificates are trusted system-wide, no special env vars needed
|
|
local follow_output
|
|
follow_output=$("${SNAC_BIN}" follow "${SNAC_DATA_DIR}" "${username}" "${owncast_actor}" 2>&1)
|
|
local follow_exit=$?
|
|
|
|
if [[ ${follow_exit} -eq 0 ]] && [[ ! "${follow_output}" =~ "cannot" ]]; then
|
|
successful=$((successful + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
# Show the first error for debugging
|
|
if [[ ${failed} -eq 1 ]]; then
|
|
log_warn "snac follow error: ${follow_output}"
|
|
fi
|
|
fi
|
|
|
|
if [[ $((successful + failed)) -gt 0 ]] && [[ $(((successful + failed) % 20)) -eq 0 ]]; then
|
|
log_info "Follows sent: $((successful + failed))/${total} (successful: ${successful})"
|
|
fi
|
|
|
|
# Throttle follow requests to prevent overwhelming snac2
|
|
sleep "${FOLLOW_DELAY}"
|
|
done
|
|
|
|
log_test "Follow requests: ${successful} successful, ${failed} failed"
|
|
|
|
# Check snac2 queue directory for pending activities
|
|
log_info "Checking snac2 queue..."
|
|
local queue_count
|
|
queue_count=$(find "${SNAC_DATA_DIR}" -name "*.json" -path "*/queue/*" 2>/dev/null | wc -l)
|
|
log_info "snac2 queue has ${queue_count} pending items"
|
|
|
|
# Give snac2 background thread time to process all pending follows
|
|
# Wait longer for more users
|
|
local wait_time=$((10 + USER_COUNT / 10))
|
|
log_info "Waiting ${wait_time}s for snac2 to process follow requests..."
|
|
sleep "${wait_time}"
|
|
}
|
|
|
|
verify_followers() {
|
|
log_info "Verifying followers..." >&2
|
|
|
|
local auth
|
|
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
|
|
local response
|
|
|
|
response=$(curl -s "http://localhost:${OWNCAST_PORT}/api/admin/followers?limit=200" \
|
|
-H "Authorization: Basic ${auth}")
|
|
|
|
local count
|
|
count=$(echo "${response}" | grep -o '"total":[0-9]*' | grep -o '[0-9]*' || echo "0")
|
|
|
|
log_test "Owncast reports ${count} followers" >&2
|
|
echo "${count}"
|
|
}
|
|
|
|
send_test_message() {
|
|
log_info "Sending test message from Owncast..."
|
|
|
|
local auth
|
|
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
|
|
local message
|
|
message="Test message sent at $(date -u +%Y-%m-%dT%H:%M:%SZ) to ${USER_COUNT} followers"
|
|
|
|
curl -s -X POST "http://localhost:${OWNCAST_PORT}/api/admin/federation/send" \
|
|
-H "Authorization: Basic ${auth}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"value\": \"${message}\"}" > /dev/null
|
|
|
|
log_info "Message sent: ${message}"
|
|
}
|
|
|
|
check_snac_inboxes_count() {
|
|
local users_with_messages=0
|
|
|
|
for username in "${SNAC_USERNAMES[@]}"; do
|
|
if user_has_message "${username}"; then
|
|
users_with_messages=$((users_with_messages + 1))
|
|
fi
|
|
done
|
|
|
|
echo "${users_with_messages}"
|
|
}
|
|
|
|
user_has_message() {
|
|
local username="$1"
|
|
local user_dir="${SNAC_DATA_DIR}/user/${username}"
|
|
|
|
# snac2 stores incoming posts in various places depending on type
|
|
# Check timeline, public, private directories for any .json files
|
|
local count
|
|
for subdir in public private timeline; do
|
|
if [[ -d "${user_dir}/${subdir}" ]]; then
|
|
count=$(find "${user_dir}/${subdir}" -name "*.json" -type f 2>/dev/null | wc -l)
|
|
if [[ "${count}" -gt 0 ]]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Also check the global object store for objects addressed to this user
|
|
# snac2 might store activities in the object directory
|
|
return 1
|
|
}
|
|
|
|
check_snac_inboxes() {
|
|
log_info "Checking snac2 user inboxes for delivered messages..." >&2
|
|
|
|
local users_with_messages=0
|
|
local users_without_messages=()
|
|
local total=${#SNAC_USERNAMES[@]}
|
|
|
|
for username in "${SNAC_USERNAMES[@]}"; do
|
|
if user_has_message "${username}"; then
|
|
users_with_messages=$((users_with_messages + 1))
|
|
else
|
|
users_without_messages+=("${username}")
|
|
fi
|
|
done
|
|
|
|
log_test "${users_with_messages}/${total} users received the message" >&2
|
|
|
|
if [[ ${#users_without_messages[@]} -gt 0 ]] && [[ ${#users_without_messages[@]} -le 10 ]]; then
|
|
log_warn "Users missing messages: ${users_without_messages[*]}" >&2
|
|
elif [[ ${#users_without_messages[@]} -gt 10 ]]; then
|
|
log_warn "Users missing messages: ${users_without_messages[*]:0:10} ... and $((${#users_without_messages[@]} - 10)) more" >&2
|
|
fi
|
|
|
|
echo "${users_with_messages}"
|
|
}
|
|
|
|
# Count followers with shared_inbox set vs those with individual inboxes only
|
|
count_shared_inbox_followers() {
|
|
local shared_count
|
|
shared_count=$(sqlite3 "${OWNCAST_DB}" "SELECT COUNT(*) FROM ap_followers WHERE shared_inbox IS NOT NULL AND shared_inbox != '';" 2>/dev/null || echo "0")
|
|
echo "${shared_count}"
|
|
}
|
|
|
|
count_individual_inbox_followers() {
|
|
local individual_count
|
|
individual_count=$(sqlite3 "${OWNCAST_DB}" "SELECT COUNT(*) FROM ap_followers WHERE shared_inbox IS NULL OR shared_inbox = '';" 2>/dev/null || echo "0")
|
|
echo "${individual_count}"
|
|
}
|
|
|
|
# Clear shared_inbox from a percentage of followers to test mixed delivery
|
|
clear_some_shared_inboxes() {
|
|
local percentage=${1:-50} # Default to 50% of followers
|
|
|
|
log_info "Clearing shared_inbox from ${percentage}% of followers to test mixed delivery..."
|
|
|
|
# Get total follower count
|
|
local total_followers
|
|
total_followers=$(sqlite3 "${OWNCAST_DB}" "SELECT COUNT(*) FROM ap_followers;" 2>/dev/null || echo "0")
|
|
|
|
if [[ "${total_followers}" -eq 0 ]]; then
|
|
log_warn "No followers found in database"
|
|
return 1
|
|
fi
|
|
|
|
# Calculate how many to clear
|
|
local clear_count=$((total_followers * percentage / 100))
|
|
|
|
log_info "Total followers: ${total_followers}, clearing shared_inbox from ${clear_count} followers"
|
|
|
|
# Clear shared_inbox from a random subset of followers
|
|
# SQLite's RANDOM() function helps select random rows
|
|
sqlite3 "${OWNCAST_DB}" "UPDATE ap_followers SET shared_inbox = NULL WHERE iri IN (SELECT iri FROM ap_followers ORDER BY RANDOM() LIMIT ${clear_count});"
|
|
|
|
local shared_remaining
|
|
shared_remaining=$(count_shared_inbox_followers)
|
|
local individual_count
|
|
individual_count=$(count_individual_inbox_followers)
|
|
|
|
log_info "After clearing: ${shared_remaining} followers with shared_inbox, ${individual_count} with individual inbox only"
|
|
}
|
|
|
|
verify_all_followers_received_message() {
|
|
local followers=$1
|
|
local max_wait=${2:-60}
|
|
|
|
log_info "Verifying all ${followers} followers receive the message (max ${max_wait}s)..." >&2
|
|
|
|
local start_time
|
|
start_time=$(date +%s)
|
|
local last_count=0
|
|
local delivery_time=0
|
|
local current_time
|
|
local count
|
|
|
|
while true; do
|
|
current_time=$(date +%s)
|
|
local elapsed=$((current_time - start_time))
|
|
|
|
if [[ ${elapsed} -ge ${max_wait} ]]; then
|
|
log_warn "Timeout after ${max_wait}s" >&2
|
|
delivery_time=${elapsed}
|
|
break
|
|
fi
|
|
|
|
count=$(check_snac_inboxes_count)
|
|
|
|
if [[ ${count} -ne ${last_count} ]]; then
|
|
local pct=$((count * 100 / followers))
|
|
log_info "Delivery progress: ${count}/${followers} (${pct}%)" >&2
|
|
last_count=${count}
|
|
fi
|
|
|
|
if [[ ${count} -ge ${followers} ]]; then
|
|
delivery_time=${elapsed}
|
|
log_test "All ${followers} followers received the message in ${delivery_time}s" >&2
|
|
echo "${delivery_time}"
|
|
return 0
|
|
fi
|
|
|
|
sleep 2
|
|
done
|
|
|
|
local final_count
|
|
final_count=$(check_snac_inboxes_count)
|
|
echo "${delivery_time}"
|
|
if [[ ${final_count} -lt ${followers} ]]; then
|
|
log_error "Only ${final_count}/${followers} followers received the message" >&2
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
print_results() {
|
|
local followers=$1
|
|
local delivered=$2
|
|
local delivery_time=$3
|
|
|
|
# Get shared inbox vs individual inbox counts
|
|
local shared_inbox_count
|
|
shared_inbox_count=$(count_shared_inbox_followers)
|
|
local individual_inbox_count
|
|
individual_inbox_count=$(count_individual_inbox_followers)
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo "ActivityPub Federation Test Results"
|
|
echo "========================================"
|
|
echo "Test Users Created: ${USER_COUNT}"
|
|
echo "Followers Registered: ${followers}"
|
|
echo " - Shared Inbox: ${shared_inbox_count}"
|
|
echo " - Individual Inbox: ${individual_inbox_count}"
|
|
echo "Messages Delivered: ${delivered}"
|
|
if [[ -n "${delivery_time}" ]] && [[ "${delivery_time}" -gt 0 ]]; then
|
|
echo "Delivery Time: ${delivery_time}s"
|
|
fi
|
|
echo ""
|
|
|
|
if [[ "${followers}" -gt 0 ]]; then
|
|
local follow_rate=$((followers * 100 / USER_COUNT))
|
|
echo "Follow Success Rate: ${follow_rate}%"
|
|
fi
|
|
|
|
if [[ "${followers}" -gt 0 ]]; then
|
|
local delivery_rate=$((delivered * 100 / followers))
|
|
echo "Delivery Rate: ${delivery_rate}%"
|
|
fi
|
|
|
|
echo ""
|
|
echo "----------------------------------------"
|
|
if [[ "${delivered}" -eq "${followers}" ]] && [[ "${followers}" -gt 0 ]]; then
|
|
echo -e "${GREEN}TEST PASSED${NC}"
|
|
echo "All ${followers} followers received the message."
|
|
elif [[ "${delivered}" -gt 0 ]]; then
|
|
echo -e "${RED}TEST FAILED${NC}"
|
|
echo "Only ${delivered} of ${followers} followers received the message."
|
|
echo "Missing: $((followers - delivered)) followers"
|
|
elif [[ "${followers}" -eq 0 ]]; then
|
|
echo -e "${RED}TEST FAILED${NC}"
|
|
echo "No followers were registered."
|
|
else
|
|
echo -e "${RED}TEST FAILED${NC}"
|
|
echo "No messages were delivered to any followers."
|
|
fi
|
|
echo "========================================"
|
|
}
|
|
|
|
main() {
|
|
# Kill any leftover processes from previous runs
|
|
kill_leftover_processes
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo "ActivityPub Federation Test"
|
|
echo "========================================"
|
|
log_info "Configuration: ${USER_COUNT} test users"
|
|
log_info "Owncast URL: ${OWNCAST_URL}"
|
|
log_info "snac2 URL: ${SNAC_URL}"
|
|
if [[ "${CLEAR_SHARED_INBOX_PERCENT}" -gt 0 ]]; then
|
|
log_info "Mixed delivery test: ${CLEAR_SHARED_INBOX_PERCENT}% of followers will use individual inbox"
|
|
fi
|
|
echo ""
|
|
|
|
# ==========================================
|
|
# STEP 1: Setup snac2 server with test users
|
|
# ==========================================
|
|
echo "----------------------------------------"
|
|
echo "STEP 1: Setup snac2 server with test users"
|
|
echo "----------------------------------------"
|
|
setup_temp_dir
|
|
check_hosts_entry
|
|
install_snac2
|
|
check_certs
|
|
init_snac2
|
|
enable_snac_shared_inbox
|
|
create_snac_users
|
|
start_proxy
|
|
start_snac2
|
|
verify_snac_shared_inbox
|
|
echo ""
|
|
|
|
# ==========================================
|
|
# STEP 2: Setup Owncast with federation
|
|
# ==========================================
|
|
echo "----------------------------------------"
|
|
echo "STEP 2: Setup Owncast with federation"
|
|
echo "----------------------------------------"
|
|
build_owncast
|
|
start_owncast
|
|
configure_owncast
|
|
sleep 2
|
|
|
|
# Optional: prompt for test stream
|
|
prompt_for_test_stream
|
|
echo ""
|
|
|
|
# ==========================================
|
|
# STEP 3: snac2 users follow Owncast
|
|
# ==========================================
|
|
echo "----------------------------------------"
|
|
echo "STEP 3: snac2 users follow Owncast"
|
|
echo "----------------------------------------"
|
|
send_follow_requests
|
|
|
|
# Wait for all followers to be registered (with timeout)
|
|
local followers=0
|
|
local max_wait=60 # Maximum seconds to wait for followers
|
|
local waited=0
|
|
local check_interval=2
|
|
|
|
log_info "Waiting for ${USER_COUNT} followers to be registered..."
|
|
while [[ "${followers}" -lt "${USER_COUNT}" ]] && [[ "${waited}" -lt "${max_wait}" ]]; do
|
|
sleep "${check_interval}"
|
|
waited=$((waited + check_interval))
|
|
followers=$(verify_followers)
|
|
if [[ "${followers}" -lt "${USER_COUNT}" ]]; then
|
|
log_info "Followers registered: ${followers}/${USER_COUNT} (waited ${waited}s)"
|
|
fi
|
|
done
|
|
|
|
if [[ "${followers}" -eq 0 ]]; then
|
|
log_error "No followers registered - cannot proceed with message delivery test"
|
|
print_results "${followers}" 0
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${followers}" -lt "${USER_COUNT}" ]]; then
|
|
log_warn "Only ${followers}/${USER_COUNT} followers registered after ${max_wait}s timeout"
|
|
else
|
|
log_info "All ${followers} followers registered"
|
|
fi
|
|
|
|
# Optionally clear shared_inbox from some followers to test mixed delivery
|
|
if [[ "${CLEAR_SHARED_INBOX_PERCENT}" -gt 0 ]]; then
|
|
echo ""
|
|
echo "----------------------------------------"
|
|
echo "STEP 3.5: Clear shared_inbox from ${CLEAR_SHARED_INBOX_PERCENT}% of followers"
|
|
echo "----------------------------------------"
|
|
clear_some_shared_inboxes "${CLEAR_SHARED_INBOX_PERCENT}"
|
|
fi
|
|
echo ""
|
|
|
|
# ==========================================
|
|
# STEP 4: Owncast sends message to followers
|
|
# ==========================================
|
|
echo "----------------------------------------"
|
|
echo "STEP 4: Owncast sends message to followers"
|
|
echo "----------------------------------------"
|
|
send_test_message
|
|
echo ""
|
|
|
|
# ==========================================
|
|
# STEP 5: Verify all followers received message
|
|
# ==========================================
|
|
echo "----------------------------------------"
|
|
echo "STEP 5: Verify all followers received message"
|
|
echo "----------------------------------------"
|
|
local delivery_success=0
|
|
local delivery_time
|
|
if delivery_time=$(verify_all_followers_received_message "${followers}" 60); then
|
|
delivery_success=1
|
|
fi
|
|
|
|
local delivered
|
|
delivered=$(check_snac_inboxes)
|
|
|
|
print_results "${followers}" "${delivered}" "${delivery_time}"
|
|
|
|
if [[ "${KEEP_RUNNING:-}" == "true" ]]; then
|
|
log_info "Keeping servers running (Ctrl+C to stop)..."
|
|
log_info " - Owncast: http://localhost:${OWNCAST_PORT} (${OWNCAST_URL})"
|
|
log_info " - snac2: http://localhost:${SNAC_PORT} (${SNAC_URL})"
|
|
wait
|
|
fi
|
|
|
|
# Exit with appropriate code
|
|
# All conditions must be met:
|
|
# 1. All follow requests succeeded (followers == USER_COUNT)
|
|
# 2. Message delivery completed successfully
|
|
# 3. All followers received the message (delivered == followers)
|
|
if [[ "${followers}" -ne "${USER_COUNT}" ]]; then
|
|
log_error "Follow count mismatch: expected ${USER_COUNT}, got ${followers}"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${delivery_success}" -ne 1 ]]; then
|
|
log_error "Message delivery failed"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${delivered}" -ne "${followers}" ]]; then
|
|
log_error "Delivery count mismatch: expected ${followers}, got ${delivered}"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "All tests passed"
|
|
exit 0
|
|
}
|
|
|
|
main "$@"
|