#!/usr/bin/env bash
# This file is part of GNU Taler
# Copyright (C) 2025, 2026 Taler Systems SA
#
# GNU Taler is free software; you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation; either version 3, or (at your option) any later
# version.
#
# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with GNU Taler; see the file COPYING.  If not, see
# <http://www.gnu.org/licenses/>
#
#
# The paywall, against a real merchant backend.
#
# test_reverse_proxy.sh runs paivana-httpd with `-n', which switches the
# paywall off before the first byte of a request is looked at, so it
# exercises the proxy and nothing else.  Everything paivana exists for --
# the paywall page, the payment, the access cookie -- is on the other side
# of that flag and had no automated coverage at all.
#
# This test puts a real GNU Taler system behind paivana-httpd with
# taler-unified-setup.sh (a fakebank, an exchange and a merchant backend,
# the same way merchant and anastasis test their own payment paths), buys
# access with taler-wallet-cli, and checks what the daemon does with it.
#
# The client half is written out by hand rather than driven through a
# browser: the paywall page's JavaScript computes a payment identifier from
# (nonce, website, expiration) and the daemon computes the same identifier
# independently, so the two agreeing IS the protocol -- it is never sent
# from one to the other.  paivana_id.py re-derives it from the definition
# src/frontend/paywall.js implements, which is what makes this a test of
# both ends rather than of one end twice.

set -eu

# shellcheck source=src/tests/setup.sh
. "$(cd -- "$(dirname -- "$0")" && pwd)/setup.sh"

# Ports of the Taler system come from test_paywall.conf and are fixed;
# paivana's own move as a block, exactly as in test_reverse_proxy.sh.
PORT_BASE="${PAIVANA_PORT_BASE:-18400}"
PAIVANA_PORT=$((PORT_BASE + 110))
ORIGIN_PORT=$((PORT_BASE + 111))
MERCHANT_PORT=9966
EXCHANGE_PORT=8081
BANK_PORT=8082

function here() {
    cd -- "$(dirname -- "$0")" && pwd
}

# Same convention as test_reverse_proxy.sh: meson passes both, and the
# defaults let the script be run by hand from either tree.
SRCDIR="${SRCDIR:-$(here)}"
BUILDDIR="${BUILDDIR:-$PWD}"

PAIVANA_HTTPD="${PAIVANA_HTTPD:-$BUILDDIR/../backend/paivana-httpd}"
UPSTREAM_MHD="${UPSTREAM_MHD:-$BUILDDIR/upstream_mhd}"

CHECKS=0
FAILS=0

function ok() {
    CHECKS=$((CHECKS + 1))
    echo "  OK: $1"
}

function fail() {
    CHECKS=$((CHECKS + 1))
    FAILS=$((FAILS + 1))
    printf '  FAIL: %b\n' "$1" >&2
}

# Is a TCP port free?  Same probe as test_reverse_proxy.sh: bash's
# /dev/tcp in a subshell, so a failed connect cannot kill the script.
function port_is_free() {
    ! (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null
}

function wait_for_port() {
    local port="$1" pid="${2:-}" tries=100
    while [ "$tries" -gt 0 ]
    do
        if ! port_is_free "$port"
        then
            return 0
        fi
        # Probe first: a child that handed the listening socket on and
        # exited is still a service that came up.
        if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null
        then
            return 1
        fi
        sleep 0.1
        tries=$((tries - 1))
    done
    return 1
}

# --------------------------------------------------------------------
# Preconditions.  Every one of these is an environment problem rather
# than a regression, so each exits 77 naming what is missing.
# --------------------------------------------------------------------

echo -n "Checking for curl ..."
curl --version >/dev/null 2>&1 || exit_skip "no curl"
echo " OK"
echo -n "Checking for jq ..."
jq --version >/dev/null 2>&1 || exit_skip "no jq"
echo " OK"
echo -n "Checking for python3 ..."
python3 --version >/dev/null 2>&1 || exit_skip "no python3"
echo " OK"
echo -n "Checking for taler-unified-setup.sh ..."
command -v taler-unified-setup.sh >/dev/null 2>&1 \
    || exit_skip "no taler-unified-setup.sh (the Taler stack is not installed)"
echo " OK"
echo -n "Checking for taler-wallet-cli ..."
command -v taler-wallet-cli >/dev/null 2>&1 || exit_skip "no taler-wallet-cli"
echo " OK"
echo -n "Checking for taler-merchant-httpd ..."
command -v taler-merchant-httpd >/dev/null 2>&1 || exit_skip "no taler-merchant-httpd"
echo " OK"
echo -n "Checking for PostgreSQL ..."
pg_isready >/dev/null 2>&1 || exit_skip "PostgreSQL is not accepting connections"
echo " OK"
echo -n "Checking for paivana-httpd ..."
[ -x "$PAIVANA_HTTPD" ] || exit_skip "no paivana-httpd at $PAIVANA_HTTPD"
echo " OK"
echo -n "Checking for the test upstream ..."
[ -x "$UPSTREAM_MHD" ] || exit_skip "no upstream_mhd at $UPSTREAM_MHD"
echo " OK"

# The paywall page is a template paivana loads from its installation
# prefix, so a build tree alone is not enough.  Rather than demanding
# `make install', stage the one file into a prefix of our own and point
# PAIVANA_PREFIX at it -- the same variable paivana's project data
# declares for exactly this.
PAYWALL_TEMPLATE=""
for cand in "$BUILDDIR/../frontend/paywall.en.must" \
            "$SRCDIR/../frontend/paywall.en.must"
do
    if [ -r "$cand" ]
    then
        PAYWALL_TEMPLATE="$cand"
        break
    fi
done
if [ -z "$PAYWALL_TEMPLATE" ]
then
    exit_skip "the paywall template has not been built"
fi

echo -n "Checking that the ports are free ..."
BUSY=""
for p in "$PAIVANA_PORT" "$ORIGIN_PORT" "$MERCHANT_PORT" "$EXCHANGE_PORT" "$BANK_PORT"
do
    port_is_free "$p" || BUSY="$BUSY $p"
done
if [ -n "$BUSY" ]
then
    # The Taler ports are the ones the merchant suite uses, so this is
    # most often another Taler test running, not a leftover.  paivana's
    # own two move with PAIVANA_PORT_BASE.
    exit_skip "port(s) already in use:$BUSY"
fi
echo " OK"

# --------------------------------------------------------------------
# Scratch space.  NOT named TMPDIR: that is the variable every child
# reads for its own temporary files, and cleanup removes this directory
# while the Taler services may still be running out of it.
# --------------------------------------------------------------------

SCRATCH="$(mktemp -d -t paivana-paywall.XXXXXX)"
PIDS=""

function stop_children() {
    local pid
    for pid in $PIDS
    do
        kill -TERM "$pid" 2>/dev/null || true
    done
    for pid in $PIDS
    do
        wait "$pid" 2>/dev/null || true
    done
    PIDS=""
}

function paywall_cleanup() {
    stop_children
    # exit_cleanup (from setup.sh) stops taler-unified-setup.sh.
    exit_cleanup
    if [ -z "${KEEP_TMP:-}" ]
    then
        rm -rf "$SCRATCH"
    else
        echo "Keeping $SCRATCH" >&2
    fi
}
trap paywall_cleanup EXIT

mkdir -p "$SCRATCH/prefix/share/paivana/templates"
cp "$PAYWALL_TEMPLATE" "$SCRATCH/prefix/share/paivana/templates/"
export PAIVANA_PREFIX="$SCRATCH/prefix/"

# --------------------------------------------------------------------
# The Taler system.
# --------------------------------------------------------------------

CONF="$SRCDIR/test_paywall.conf"
[ -r "$CONF" ] || exit_fail "cannot read $CONF"

# taler-unified-setup.sh resolves TALER_TEST_HOME relative to the working
# directory and writes into it, so run it from the scratch directory with
# an absolute path to the configuration.
CONF="$(cd "$(dirname "$CONF")" && pwd)/$(basename "$CONF")"
cd "$SCRATCH"

# Give the exchange and the merchant a database of their own.  Reusing
# `talercheck' would have this test and the merchant's own tests
# clobbering each other's tables.
DB=paivanacheck
if ! psql -Aqt -c "SELECT 1" "$DB" >/dev/null 2>&1
then
    createdb "$DB" >/dev/null 2>&1 \
        || exit_skip "cannot create the PostgreSQL database $DB"
fi

setup -c "$CONF" \
      -r "merchant-exchange-default" \
      -em \
      -f -d x-taler-bank -u exchange-account-2

MERCHANT_URL="http://localhost:$MERCHANT_PORT/"
AUTH="Authorization: Bearer secret-token:super_secret"
RESP="$SCRATCH/response.json"

function merchant_post() {
    local path="$1" body="$2"
    curl -s -o "$RESP" -w "%{http_code}" \
         -H "Content-Type: application/json" \
         -H "$AUTH" \
         -X POST \
         "${MERCHANT_URL}${path}" \
         -d "$body"
}

echo -n "Configuring merchant instance ..."
STATUS=$(merchant_post management/instances \
    '{"auth":{"method":"token","token":"secret-token:super_secret"},
      "id":"admin","name":"default","user_type":"business",
      "address":{},"jurisdiction":{},"use_stefan":true,
      "default_wire_transfer_delay":{"d_us":50000000000},
      "default_pay_delay":{"d_us":60000000000}}')
[ "$STATUS" = "204" ] || exit_fail "creating the instance: got $STATUS, $(cat "$RESP")"
echo " OK"

echo -n "Configuring merchant bank account ..."
STATUS=$(merchant_post private/accounts \
    '{"payto_uri":"payto://x-taler-bank/localhost/fortythree?receiver-name=fortythree"}')
[ "$STATUS" = "200" ] || exit_fail "adding the account: got $STATUS, $(cat "$RESP")"
echo " OK"

# website_regex is `.*' so that every URL is paywalled and WHITELIST is
# the only exemption; that is what lets the whitelist cases below mean
# what they say.  With a narrower expression a URL matching no template
# is served for free, which would make a whitelist miss indistinguishable
# from a template miss.
echo -n "Creating the Paivana template ..."
STATUS=$(merchant_post private/templates \
    '{"template_id":"premium",
      "template_description":"Paywalled content",
      "template_contract":{"template_type":"paivana",
                           "summary":"Access to the article",
                           "website_regex":".*",
                           "max_pickup_duration":{"d_us":3600000000},
                           "choices":[{"amount":"TESTKUDOS:1"}]}}')
[ "$STATUS" = "204" ] || exit_fail "creating the template: got $STATUS, $(cat "$RESP")"
echo " OK"

# --------------------------------------------------------------------
# The origin and paivana itself.
# --------------------------------------------------------------------

BASE_URL="http://localhost:$PAIVANA_PORT/"

cat > "$SCRATCH/paivana.conf" <<EOF
[paivana]
DESTINATION_BASE_URL = http://localhost:$ORIGIN_PORT/
BASE_URL = $BASE_URL
MERCHANT_BACKEND_URL = $MERCHANT_URL
MERCHANT_ACCESS_TOKEN = secret-token:super_secret
# A fixed secret so that a cookie minted here can be reasoned about; a
# real deployment must not do this (see the manual on SECRET).
SECRET = paivana-integration-test-secret
WHITELIST = /echo-headers|/large/.*
SERVE = tcp
PORT = $PAIVANA_PORT
EOF

echo -n "Starting the origin ..."
"$UPSTREAM_MHD" "$ORIGIN_PORT" > "$SCRATCH/origin.log" 2>&1 &
ORIGIN_PID=$!
PIDS="$PIDS $ORIGIN_PID"
wait_for_port "$ORIGIN_PORT" "$ORIGIN_PID" \
    || exit_fail "the test origin did not start on port $ORIGIN_PORT"
echo " OK"

echo -n "Starting paivana-httpd ..."
"$PAIVANA_HTTPD" -c "$SCRATCH/paivana.conf" -L INFO > "$SCRATCH/paivana.log" 2>&1 &
PAIVANA_PID=$!
PIDS="$PIDS $PAIVANA_PID"
# paivana does not listen until it has fetched the templates, so this
# also asserts that the merchant handed them over.
wait_for_port "$PAIVANA_PORT" "$PAIVANA_PID" \
    || exit_fail "paivana-httpd did not start:\n$(cat "$SCRATCH/paivana.log")"
echo " OK"

# --------------------------------------------------------------------
# What the paywall does before anyone has paid.
# --------------------------------------------------------------------

echo "-- before payment --"

STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}hello")
if [ "$STATUS" = "302" ]
then
    ok "an unpaid URL is redirected to the paywall"
else
    fail "an unpaid URL should be 302, got $STATUS"
fi

curl -s -D "$SCRATCH/redirect.hdr" -o /dev/null "${BASE_URL}hello"
LOCATION=$(grep -i '^location:' "$SCRATCH/redirect.hdr" | tr -d '\r' | cut -d' ' -f2-)
case "$LOCATION" in
    *"/.well-known/paivana/templates/premium#"*)
        ok "the redirect names the template and carries the website in the fragment"
        ;;
    *)
        fail "unexpected paywall redirect: $LOCATION"
        ;;
esac

# The fragment is the website, base64url-encoded; the page's JavaScript
# atob()s it, so a '-' or '_' in it has to survive.
FRAGMENT="${LOCATION#*#}"
DECODED=$(python3 -c '
import base64, sys
s = sys.argv[1]
print(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)).decode("utf-8"))' "$FRAGMENT")
if [ "$DECODED" = "${BASE_URL}hello" ]
then
    ok "the fragment decodes to the URL that was asked for"
else
    fail "fragment decodes to '$DECODED', want '${BASE_URL}hello'"
fi

PAGE_STATUS=$(curl -s -D "$SCRATCH/page.hdr" -o "$SCRATCH/page.html" \
                   -w "%{http_code}" \
                   "${BASE_URL}.well-known/paivana/templates/premium")
if [ "$PAGE_STATUS" = "402" ]
then
    ok "the paywall page is served with 402 Payment Required"
else
    fail "the paywall page should be 402, got $PAGE_STATUS"
fi

PAIVANA_HEADER=$(grep -i '^paivana:' "$SCRATCH/page.hdr" | tr -d '\r' | cut -d' ' -f2-)
case "$PAIVANA_HEADER" in
    "taler+http://pay-template/localhost:$MERCHANT_PORT/premium")
        ok "the Paivana header carries the pay-template URI"
        ;;
    *)
        fail "unexpected Paivana header: '$PAIVANA_HEADER'"
        ;;
esac

if grep -qi "^content-security-policy:.*default-src 'none'" "$SCRATCH/page.hdr"
then
    ok "the paywall page carries a Content-Security-Policy"
else
    fail "the paywall page has no restrictive CSP"
fi

# --------------------------------------------------------------------
# The whitelist, which only exists on this side of the paywall.
#
# README and paivana.conf(5) both promise that a WHITELIST expression is
# matched against the whole path and anchored at both ends, so that
# `/free/' waives nothing while `/free/.*' waives a subtree.  The
# regexec that decides it sits behind the paywall `-n' switches off, so
# no other test in the tree can reach these.
# --------------------------------------------------------------------

echo "-- the whitelist --"

function expect_forwarded() {
    local path="$1" what="$2" status
    status=$(curl -s -D "$SCRATCH/w.hdr" -o /dev/null -w "%{http_code}" "${BASE_URL}${path}")
    if [ "302" = "$status" ]
    then
        fail "$what: /$path was paywalled (302)"
    elif grep -qi '^x-upstream:' "$SCRATCH/w.hdr"
    then
        ok "$what"
    else
        fail "$what: /$path answered $status but not by the origin"
    fi
}

function expect_paywalled() {
    local path="$1" what="$2" status
    status=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}${path}")
    if [ "302" = "$status" ]
    then
        ok "$what"
    else
        fail "$what: /$path answered $status, want 302"
    fi
}

expect_forwarded "echo-headers" "a whitelisted path is served without payment"
expect_forwarded "large/64" "an alternation branch is whitelisted too"
expect_paywalled "x/echo-headers" \
    "a path merely containing the whitelisted one is not waived (left anchor)"
expect_paywalled "echo-headers/x" \
    "a path extending the whitelisted one is not waived (right anchor)"
expect_paywalled "hello" "a path outside the whitelist is paywalled"

# --------------------------------------------------------------------
# The payment endpoint's refusals, before we have anything valid to send.
# --------------------------------------------------------------------

echo "-- the payment endpoint --"

STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
              -H "Content-Type: application/json" \
              "${BASE_URL}.well-known/paivana" -d '{}')
if [ "$STATUS" = "400" ]
then
    ok "a redemption without the required fields is refused with 400"
else
    fail "an empty redemption should be 400, got $STATUS"
fi

# A nonce is 16 bytes in GNUnet's Crockford base32, which is 26
# characters; anything else must not reach the order lookup.  25 zeroes
# is the off-by-one an implementer would write.
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
              -H "Content-Type: application/json" \
              "${BASE_URL}.well-known/paivana" \
              -d "$(jq -n --arg w "${BASE_URL}hello" \
                    '{order_id:"no-such-order",nonce:"0000000000000000000000000",
                      expiration:{t_s:2000000000},website:$w}')")
if [ "$STATUS" = "400" ]
then
    ok "a redemption with a nonce of the wrong length is refused with 400"
else
    fail "a short nonce should be 400, got $STATUS"
fi

# Well-formed, but naming an order the merchant has never heard of: this
# has to reach the backend and come back as a refusal rather than as a
# parse error, which is what tells the two apart in a log.
read -r JUNK_NONCE _ < <(python3 "$SRCDIR/paivana_id.py" 2000000000 "${BASE_URL}hello" \
                                 000102030405060708090a0b0c0d0e0f)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
              -H "Content-Type: application/json" \
              "${BASE_URL}.well-known/paivana" \
              -d "$(jq -n --arg w "${BASE_URL}hello" --arg n "$JUNK_NONCE" \
                    '{order_id:"no-such-order",nonce:$n,
                      expiration:{t_s:2000000000},website:$w}')")
if [ "$STATUS" = "404" ] || [ "$STATUS" = "409" ]
then
    ok "a redemption naming an order that was never paid is refused ($STATUS)"
else
    fail "an unknown order should be 404 or 409, got $STATUS"
fi

# --------------------------------------------------------------------
# Buy access.
# --------------------------------------------------------------------

echo "-- paying --"

WALLET="$SCRATCH/wallet.sqlite3"

echo -n "Withdrawing test coins ..."
taler-wallet-cli --no-throttle --wallet-db="$WALLET" \
    api --expect-success 'withdrawTestBalance' \
    "$(jq -n --arg bank "http://localhost:$BANK_PORT/" \
             --arg exchange "http://localhost:$EXCHANGE_PORT/" \
        '{amount:"TESTKUDOS:20",corebankApiBaseUrl:$bank,exchangeBaseUrl:$exchange}')" \
    > "$SCRATCH/withdraw.out" 2> "$SCRATCH/withdraw.err" \
    || exit_fail "withdrawTestBalance failed:\n$(cat "$SCRATCH/withdraw.err")"
taler-exchange-wirewatch -a exchange-account-2 -c "$CONF" -t \
    > "$SCRATCH/wirewatch.log" 2>&1 \
    || exit_fail "wirewatch failed:\n$(cat "$SCRATCH/wirewatch.log")"
timeout 240 taler-wallet-cli --no-throttle --wallet-db="$WALLET" run-until-done \
    > "$SCRATCH/withdraw2.out" 2> "$SCRATCH/withdraw2.err" \
    || exit_fail "the withdrawal did not finish:\n$(cat "$SCRATCH/withdraw2.err")"
echo " OK"

WEBSITE="${BASE_URL}hello"
# The page computes this as `now + max_pickup_duration'; one hour is what
# the template above allows.
EXPIRATION=$(( $(date +%s) + 3600 ))
read -r NONCE PAIVANA_ID < <(python3 "$SRCDIR/paivana_id.py" "$EXPIRATION" "$WEBSITE")
echo "  paivana_id: $PAIVANA_ID"

function urlenc() {
    python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$1"
}

PAY_URI="taler+http://pay-template/localhost:$MERCHANT_PORT/premium"
PAY_URI="$PAY_URI?session_id=$(urlenc "$PAIVANA_ID")"
PAY_URI="$PAY_URI&fulfillment_url=$(urlenc "$WEBSITE")"

echo -n "Paying ..."
# "0" selects the first (and only) choice.
echo "0" | timeout 240 taler-wallet-cli --no-throttle --wallet-db="$WALLET" \
    handle-uri "$PAY_URI" -y > "$SCRATCH/pay.out" 2> "$SCRATCH/pay.err" \
    || exit_fail "paying failed:\n$(cat "$SCRATCH/pay.err")"
echo " OK"

# This is what the paywall page polls: the merchant reports the order it
# has seen paid under our session, which is the paivana ID.
SESSION_URL="${MERCHANT_URL}sessions/$(urlenc "$PAIVANA_ID")"
SESSION_URL="$SESSION_URL?fulfillment_url=$(urlenc "$WEBSITE")&timeout_ms=10000"
STATUS=$(curl -s -o "$RESP" -w "%{http_code}" "$SESSION_URL")
ORDER_ID=$(jq -r '.order_id // empty' < "$RESP")
if [ "$STATUS" = "200" ] && [ -n "$ORDER_ID" ]
then
    ok "the merchant reports the order paid under the paivana ID we derived"
else
    fail "the merchant did not report a paid order: $STATUS, $(cat "$RESP")"
    echo "=== $CHECKS checks, $FAILS failures ==="
    exit 1
fi

# --------------------------------------------------------------------
# Redeem it.
# --------------------------------------------------------------------

echo "-- redeeming --"

function redeem() {
    local website="$1" expiration="$2" nonce="$3" order="$4" hdr="$5"
    curl -s -D "$hdr" -o /dev/null -w "%{http_code}" -X POST \
         -H "Content-Type: application/json" \
         "${BASE_URL}.well-known/paivana" \
         -d "$(jq -n --arg o "$order" --arg n "$nonce" \
                   --argjson e "$expiration" --arg w "$website" \
               '{order_id:$o,nonce:$n,expiration:{t_s:$e},website:$w}')"
}

STATUS=$(redeem "$WEBSITE" "$EXPIRATION" "$NONCE" "$ORDER_ID" "$SCRATCH/redeem.hdr")
if [ "$STATUS" = "303" ]
then
    ok "a paid order is redeemed with 303"
else
    fail "redemption should be 303, got $STATUS"
fi

COOKIE=$(grep -i '^set-cookie:' "$SCRATCH/redeem.hdr" | tr -d '\r' \
             | sed -n 's/.*\(Paivana-Cookie=[^;]*\).*/\1/p')
if [ -n "$COOKIE" ]
then
    ok "the redemption sets an access cookie"
else
    fail "no Paivana-Cookie in the redemption response"
fi

if grep -qi '^set-cookie:.*HttpOnly' "$SCRATCH/redeem.hdr"
then
    ok "the access cookie is HttpOnly"
else
    fail "the access cookie is not HttpOnly"
fi

REDIRECT=$(grep -i '^location:' "$SCRATCH/redeem.hdr" | tr -d '\r' | cut -d' ' -f2-)
if [ "$REDIRECT" = "$WEBSITE" ]
then
    ok "the redemption redirects back to the paid URL"
else
    fail "redemption redirects to '$REDIRECT', want '$WEBSITE'"
fi

# Nothing the client sends is taken on trust.  Naming a different website
# changes the paivana ID -- it is derived from the website -- so the
# merchant is asked about a session it has never seen paid.
STATUS=$(redeem "${BASE_URL}item" "$EXPIRATION" "$NONCE" "$ORDER_ID" "$SCRATCH/x.hdr")
if [ "$STATUS" = "303" ]
then
    fail "the order was redeemed for a URL it did not pay for"
else
    ok "another website derives another paivana ID, so the order is not found ($STATUS)"
fi

# That check passes because of the session binding, and would pass even
# if check_contract() never looked at the contract.  This one reaches the
# fulfillment-URL comparison itself: buy an order whose session IS the one
# we will claim, but whose fulfillment URL is somewhere else entirely.
# The merchant will sell it -- website_regex is `.*' -- and the session
# lookup will succeed, so the only thing standing between this and a
# cookie for a page nobody paid for is paivana comparing the contract's
# fulfillment URL against the website being claimed.
echo "-- an order bought for somewhere else --"

TARGET="${BASE_URL}item"
ELSEWHERE="${BASE_URL}elsewhere"
EXP2=$(( $(date +%s) + 3600 ))
read -r NONCE2 PAIVANA_ID2 < <(python3 "$SRCDIR/paivana_id.py" "$EXP2" "$TARGET")

STATUS=$(curl -s -o "$RESP" -w "%{http_code}" \
              -H "Content-Type: application/json" \
              -X POST "${MERCHANT_URL}templates/premium" \
              -d "$(jq -n --arg w "$ELSEWHERE" --arg s "$PAIVANA_ID2" \
                    '{template_type:"paivana",website:$w,paivana_id:$s}')")
if [ "$STATUS" != "200" ]
then
    fail "could not create the mismatched order: $STATUS, $(cat "$RESP")"
else
    ORDER2=$(jq -r '.order_id' < "$RESP")
    STATUS=$(curl -s -o "$RESP" -w "%{http_code}" -H "$AUTH" \
                  "${MERCHANT_URL}private/orders/${ORDER2}?session_id=$(urlenc "$PAIVANA_ID2")")
    PAY2=$(jq -r '.taler_pay_uri // empty' < "$RESP")
    if [ -z "$PAY2" ]
    then
        fail "no pay URI for the mismatched order: $STATUS, $(cat "$RESP")"
    else
        echo "0" | timeout 240 taler-wallet-cli --no-throttle --wallet-db="$WALLET" \
            handle-uri "$PAY2" -y > "$SCRATCH/pay2.out" 2> "$SCRATCH/pay2.err" \
            || fail "paying the mismatched order failed:\n$(cat "$SCRATCH/pay2.err")"

        # Confirm the trap is armed: the merchant really does report this
        # order as paid under the session we are about to claim.
        STATUS=$(curl -s -o "$RESP" -w "%{http_code}" \
                      "${MERCHANT_URL}sessions/$(urlenc "$PAIVANA_ID2")?fulfillment_url=$(urlenc "$ELSEWHERE")&timeout_ms=10000")
        if [ "$STATUS" = "200" ] && [ -n "$(jq -r '.order_id // empty' < "$RESP")" ]
        then
            ok "the mismatched order is paid and bound to the session we will claim"
        else
            fail "the mismatched order was not paid: $STATUS, $(cat "$RESP")"
        fi

        STATUS=$(redeem "$TARGET" "$EXP2" "$NONCE2" "$ORDER2" "$SCRATCH/mm.hdr")
        if [ "$STATUS" = "303" ]
        then
            fail "an order bought for $ELSEWHERE minted a cookie for $TARGET"
        else
            ok "an order whose contract names another URL is refused ($STATUS)"
        fi

        STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET")
        if [ "$STATUS" = "302" ]
        then
            ok "and that URL is still paywalled afterwards"
        else
            fail "$TARGET answered $STATUS after the refused redemption"
        fi
    fi
fi

# The expiration is hashed into the paivana ID, so asking for a longer one
# than was paid for cannot match the session the merchant knows about.
STATUS=$(redeem "$WEBSITE" $((EXPIRATION + 86400)) "$NONCE" "$ORDER_ID" "$SCRATCH/x.hdr")
if [ "$STATUS" = "303" ]
then
    fail "the order was redeemed for a later expiration than it paid for"
else
    ok "redeeming for a later expiration is refused ($STATUS)"
fi

# --------------------------------------------------------------------
# What the cookie is worth.
# --------------------------------------------------------------------

echo "-- after payment --"

STATUS=$(curl -s -D "$SCRATCH/paid.hdr" -o "$SCRATCH/paid.body" \
              -w "%{http_code}" -H "Cookie: $COOKIE" "$WEBSITE")
if [ "$STATUS" = "200" ] && grep -qi '^x-upstream:' "$SCRATCH/paid.hdr"
then
    ok "the paid URL is served from the origin"
else
    fail "the paid URL answered $STATUS, and not from the origin"
fi

if grep -q "Hello" "$SCRATCH/paid.body"
then
    ok "the origin's body arrives intact"
else
    fail "the origin's body is not what it serves directly"
fi

STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Cookie: $COOKIE" "${BASE_URL}item")
if [ "$STATUS" = "302" ]
then
    ok "the cookie does not open a URL it was not minted for"
else
    fail "the cookie opened /item as well: $STATUS"
fi

STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$WEBSITE")
if [ "$STATUS" = "302" ]
then
    ok "without the cookie the same URL is still paywalled"
else
    fail "the paid URL is open to everyone: $STATUS"
fi

# The expiration is the KDF salt, so rewriting it in the value has to
# break the MAC rather than extend the access.
LATER=$(( EXPIRATION + 86400 ))
TAMPERED="Paivana-Cookie=${LATER}-${COOKIE#*-}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Cookie: $TAMPERED" "$WEBSITE")
if [ "$STATUS" = "302" ]
then
    ok "rewriting the expiration in the cookie invalidates it"
else
    fail "a cookie with a rewritten expiration was accepted: $STATUS"
fi

STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
              -H "Cookie: Paivana-Cookie=not-a-cookie" "$WEBSITE")
if [ "$STATUS" = "302" ]
then
    ok "a malformed cookie is refused rather than mis-parsed"
else
    fail "a malformed cookie answered $STATUS"
fi

# One payment can be redeemed again, from anywhere: this is deliberate
# (design document 076, "Payment buys access, not a seat") and the check
# is here so that a change of mind about it shows up as a test failure
# rather than as a silent change of policy.
STATUS=$(redeem "$WEBSITE" "$EXPIRATION" "$NONCE" "$ORDER_ID" "$SCRATCH/again.hdr")
if [ "$STATUS" = "303" ]
then
    ok "the redemption is repeatable, as the design document says it is"
else
    fail "redeeming a second time gave $STATUS; if that is intended, 076 needs updating"
fi

echo "=== $CHECKS checks, $FAILS failure(s) ==="
if [ "$FAILS" -gt 0 ]
then
    exit 1
fi
echo "=== all paywall tests passed ==="
exit 0
