This commit is contained in:
parent
284f8d3e49
commit
70668af8f5
20 changed files with 2354 additions and 1048 deletions
270
tools/e2e.sh
270
tools/e2e.sh
|
|
@ -60,7 +60,7 @@ trap cleanup EXIT INT TERM
|
|||
# Deterministic environment: a developer shell that sourced the repo .env
|
||||
# must not leak real provider keys into the test server — live Sendcloud
|
||||
# rates would silently change the shipping totals asserted below.
|
||||
unset MOLLIE_API_KEY BUNQ_API_KEY SENDCLOUD_PUBLIC_KEY SENDCLOUD_SECRET_KEY SENDCLOUD_METHOD 2>/dev/null || true
|
||||
unset MOLLIE_API_KEY COINGATE_API_KEY SENDCLOUD_PUBLIC_KEY SENDCLOUD_SECRET_KEY SENDCLOUD_METHOD 2>/dev/null || true
|
||||
|
||||
# An ephemeral GPG key so invoice signing runs the REAL signing path and the
|
||||
# suite can verify the signature. gpg is required (CI installs gnupg with the
|
||||
|
|
@ -86,7 +86,35 @@ chmod +x "$WORK/sendmail"
|
|||
export MAIL_COMMAND="$WORK/sendmail"
|
||||
export MAIL_FROM='Catcrafts <info@catcrafts.net>'
|
||||
|
||||
"$SERVER" --serve "$PORT" --orders="$ORDERS" --rail=fake >"$WORK/server.log" 2>&1 &
|
||||
# The shipping rate table. Shipping has no compiled-in fallback any more — the
|
||||
# carrier table is the only source of prices — so without this file every
|
||||
# checkout correctly refuses and the whole order suite would be testing the
|
||||
# refusal path by accident.
|
||||
#
|
||||
# This is byte-for-byte the cache the daily Sendcloud refresh writes, so the
|
||||
# suite drives the production lookup with no test-only hook that could drift
|
||||
# from it: country -> [[maxWeightGrams, consumerCents], ...], prices already
|
||||
# VAT-inclusive (the gross-up happens at fetch, not at load).
|
||||
#
|
||||
# The single-unit rates are the €15 / €25 / €55 the totals below assert. The
|
||||
# second band exists so the too-heavy refusal has a real ceiling to hit:
|
||||
# 10 kg / 700 g per unit = 14 units per parcel.
|
||||
cat >"$ORDERS.shipping.json" <<'JSON'
|
||||
{"method":"e2e fixture","fetched_at":"2026-01-01T00:00:00Z","per_country":{
|
||||
"NL":[[2000,1500],[10000,2900]],
|
||||
"DE":[[2000,2500],[10000,4200]],
|
||||
"GB":[[2000,5500],[10000,7900]]}}
|
||||
JSON
|
||||
|
||||
# BOTH slots on the fake rail, so the suite covers the payment CHOICE as well
|
||||
# as the lifecycle: that the form offers it, that the picked rail is what gets
|
||||
# written to the ledger, and that an order polls the provider that issued its
|
||||
# link. Which rail is behind each slot is exactly the part these tests should
|
||||
# not care about — that is what makes the same assertions valid for Mollie and
|
||||
# CoinGate. Both fakes share one marker file, so touching it below settles
|
||||
# whichever orders are outstanding.
|
||||
"$SERVER" --serve "$PORT" --orders="$ORDERS" --rail=fake --crypto-rail=fake-crypto \
|
||||
>"$WORK/server.log" 2>&1 &
|
||||
SRV_PID=$!
|
||||
|
||||
# Wait for the listener rather than sleeping a fixed amount: a fixed sleep is
|
||||
|
|
@ -256,16 +284,32 @@ else
|
|||
fi
|
||||
body_has /shop/fp6-pmos '"price":"563.30"' "schema price is the checkout integer"
|
||||
# Merchant-grade offer fields: what Merchant Center's website-crawl feed
|
||||
# reads. Shipping uses the static zone rates (listing may overstate, never
|
||||
# understate what checkout charges); returns mirror the terms page.
|
||||
# reads. Shipping is published from the live carrier table at one unit's
|
||||
# weight — the same integers checkout charges, so the listing cannot quote a
|
||||
# rate the till won't honour; returns mirror the terms page.
|
||||
body_has /shop/fp6-pmos 'OfferShippingDetails' "offers carry shipping details"
|
||||
body_has /shop/fp6-pmos 'MerchantReturnPolicy' "offers carry a return policy"
|
||||
body_has /shop/fp6-pmos '"sku":"fp6-pmos-green"' "offers carry per-variant skus"
|
||||
body_has /shop/fp6-pmos '"brand":{"@type":"Brand","name":"Fairphone"}' "product carries the hardware brand"
|
||||
# One entry per (transit tier, price) the carrier table produces — three, for
|
||||
# the fixture's NL/DE/GB. Not a fixed property of the code any more: it is
|
||||
# whatever the carrier prices, which is the point.
|
||||
if extract_ld /shop/fp6-pmos | jq -e '.hasVariant[0].offers.shippingDetails | length == 3' >/dev/null 2>&1; then
|
||||
ok "shipping details cover all three zones"
|
||||
ok "shipping details group the carrier's rates"
|
||||
else
|
||||
bad "shipping zones" "expected NL + EU + world tiers in the first variant's offer"
|
||||
bad "shipping groups" "expected 3 rate groups from the fixture table in the first variant's offer"
|
||||
fi
|
||||
# The advertised rate IS the carrier's single-unit price, and a destination the
|
||||
# table does not cover is never advertised.
|
||||
if extract_ld /shop/fp6-pmos | jq -e '[.hasVariant[0].offers.shippingDetails[].shippingRate.value] | sort == ["15.00","25.00","55.00"]' >/dev/null 2>&1; then
|
||||
ok "published shipping rates come from the carrier table"
|
||||
else
|
||||
bad "shipping rates" "expected the fixture's 15.00/25.00/55.00 in the offer"
|
||||
fi
|
||||
if extract_ld /shop/fp6-pmos | jq -e '[.hasVariant[0].offers.shippingDetails[].shippingDestination.addressCountry[]] | index("AU") == null' >/dev/null 2>&1; then
|
||||
ok "an uncovered destination is not advertised"
|
||||
else
|
||||
bad "shipping coverage" "AU has no carrier rate but appears in the listing"
|
||||
fi
|
||||
if [ "$SHOP_OPEN" = 1 ]; then
|
||||
body_has /shop/fp6-pmos 'schema.org/InStock' "open shop maps to InStock availability"
|
||||
|
|
@ -289,15 +333,19 @@ fi
|
|||
|
||||
# The shop card: one euro number as the crawler/no-JS text, every supported
|
||||
# currency pre-formatted server-side as a data attribute for the script to
|
||||
# pick from. Converted amounts carry "~". CAD converts the ex-VAT price;
|
||||
# SEK (an EU member's currency) converts the VAT-inclusive price.
|
||||
# pick from. Converted amounts carry "~". GBP converts the ex-VAT price;
|
||||
# SEK (an EU member's currency) converts the VAT-inclusive price. USD and CAD
|
||||
# are absent on purpose — the shop refuses those destinations, so it does not
|
||||
# quote a local price to them either.
|
||||
body_has /shop 'class="price__single"' "shop card renders the single-number price"
|
||||
body_has /shop 'data-cad="~CA$' "shop card carries a CAD conversion"
|
||||
body_has /shop 'data-gbp="~£' "shop card carries a GBP conversion"
|
||||
body_has /shop 'data-sek="~kr ' "shop card carries an SEK conversion"
|
||||
body_has /shop 'data-world="€465.54"' "shop card carries the euro export fallback"
|
||||
# The product page gets the same headline element, so a Canadian sees ~CA$
|
||||
body_lacks /shop 'data-usd=' "no USD price for a country the shop refuses"
|
||||
body_lacks /shop 'data-cad=' "no CAD price for a country the shop refuses"
|
||||
# The product page gets the same headline element, so a British visitor sees ~£
|
||||
# at the top there too, and the buy card states the customs position plainly.
|
||||
body_has /shop/fp6-pmos 'data-cad="~CA$' "product page headline carries the conversion"
|
||||
body_has /shop/fp6-pmos 'data-gbp="~£' "product page headline carries the conversion"
|
||||
body_has /shop/fp6-pmos 'indicative only' "buy card says converted prices are indicative"
|
||||
body_has /shop/fp6-pmos 'customs authority' "buy card names whose problem import charges are"
|
||||
body_lacks /shop/fp6-pmos 'collected on arrival' "the vague customs phrasing is gone"
|
||||
|
|
@ -680,6 +728,13 @@ if grep -q '"country":"NL"' "$ORDERS" && grep -q '"total_minor":57830' "$ORDERS"
|
|||
else
|
||||
bad "order storage" "expected NL total_minor 57830 in $ORDERS"
|
||||
fi
|
||||
# No `pay` field in that submission, which is what a form with only one rail
|
||||
# configured posts: it must land on the bank rail rather than nothing.
|
||||
if grep -q '"pay_choice":"bank"' "$ORDERS"; then
|
||||
ok "a submission with no payment choice records bank"
|
||||
else
|
||||
bad "default payment choice" "expected pay_choice bank in $ORDERS"
|
||||
fi
|
||||
|
||||
# The order page: awaiting payment, pay link, reference, self-refreshing,
|
||||
# never indexed, never cached.
|
||||
|
|
@ -700,43 +755,100 @@ status /order/00000000000000000000000000000000 404
|
|||
status /order/not-a-token 404
|
||||
status /order/deadbeef 404
|
||||
|
||||
# ── the payment choice ────────────────────────────────────────────────
|
||||
#
|
||||
# Both slots are configured here, so the form must offer both and the picked
|
||||
# one must survive all the way into the ledger. The ledger is the assertion
|
||||
# that matters: it is what the reconciler later reads to decide WHICH provider
|
||||
# may confirm the order, so a choice that renders but is not stored would mean
|
||||
# crypto orders being asked about at Mollie.
|
||||
body_has /shop/fp6-pmos 'name="pay"' "the form offers a payment choice"
|
||||
body_has /shop/fp6-pmos 'value="crypto"' "crypto is one of the choices"
|
||||
body_has /shop/fp6-pmos 'value="bank" checked' "bank is the pre-selected choice"
|
||||
|
||||
LOC_C=$(curl -s -o /dev/null -w '%{redirect_url}' -X POST \
|
||||
-d "$GOOD&pay=crypto" "$BASE/shop/fp6-pmos")
|
||||
TOKEN_C=$(printf '%s' "$LOC_C" | grep -oE '/order/[0-9a-f]{32}$' | cut -d/ -f3 || true)
|
||||
if [ -n "$TOKEN_C" ]; then
|
||||
ok "a crypto order goes through"
|
||||
if grep -q "\"id\":\"$TOKEN_C\".*\"pay_choice\":\"crypto\"" "$ORDERS"; then
|
||||
ok "the crypto choice is what the ledger records"
|
||||
else
|
||||
bad "crypto choice storage" "no pay_choice crypto for $TOKEN_C in $ORDERS"
|
||||
fi
|
||||
# The order page has to promise what is actually behind the button — the
|
||||
# bank copy on a crypto order would send someone looking for iDEAL.
|
||||
CRYPTO_HTML=$(curl -s "$BASE/order/$TOKEN_C")
|
||||
if printf '%s' "$CRYPTO_HTML" | grep -qF 'Lightning'; then
|
||||
ok "the crypto order page describes the crypto invoice"
|
||||
else
|
||||
bad "crypto order copy" "no crypto wording on /order/$TOKEN_C"
|
||||
fi
|
||||
if printf '%s' "$CRYPTO_HTML" | grep -qF 'iDEAL'; then
|
||||
bad "crypto order copy" "the crypto order page still offers iDEAL"
|
||||
else
|
||||
ok "the crypto order page does not promise iDEAL"
|
||||
fi
|
||||
else
|
||||
bad "crypto checkout" "no token from Location: $LOC_C"
|
||||
fi
|
||||
|
||||
# A payment method nobody offers is refused, and refused as a FIELD error so
|
||||
# the form comes back with the choice highlighted rather than a bare 400.
|
||||
BOGUS_CODE=$(curl -s -o "$WORK/bogus-pay.html" -w '%{http_code}' -X POST \
|
||||
-d "$GOOD&pay=invoice-me-later" "$BASE/shop/fp6-pmos")
|
||||
if [ "$BOGUS_CODE" = "422" ]; then
|
||||
ok "an unknown payment method is refused"
|
||||
else
|
||||
bad "unknown payment method" "expected 422, got $BOGUS_CODE"
|
||||
fi
|
||||
if grep -qF 'Pick one of the payment methods' "$WORK/bogus-pay.html"; then
|
||||
ok "the refusal names the payment field"
|
||||
else
|
||||
bad "unknown payment method" "no payment field error in the response"
|
||||
fi
|
||||
|
||||
# A non-EU order: ex-VAT goods, world shipping, and the indicative national
|
||||
# currency line sourced from the build-time ECB rates.
|
||||
LOC_CA=$(curl -s -o /dev/null -w '%{redirect_url}' -X POST -d 'email=ca%40example.org&name=Terry&street=1%20Bloor%20St&postal=M4W&city=Toronto&country=CA' "$BASE/shop/fp6-pmos")
|
||||
TOKEN_CA=$(printf '%s' "$LOC_CA" | grep -oE '/order/[0-9a-f]{32}$' | cut -d/ -f3 || true)
|
||||
if [ -n "$TOKEN_CA" ]; then
|
||||
CA_HTML=$(curl -s "$BASE/order/$TOKEN_CA")
|
||||
# currency line sourced from the build-time ECB rates. GB rather than a North
|
||||
# American destination because those are refused outright (see the block below).
|
||||
LOC_GB=$(curl -s -o /dev/null -w '%{redirect_url}' -X POST -d 'email=gb%40example.org&name=Terry&street=1%20Baker%20St&postal=W1U&city=London&country=GB' "$BASE/shop/fp6-pmos")
|
||||
TOKEN_GB=$(printf '%s' "$LOC_GB" | grep -oE '/order/[0-9a-f]{32}$' | cut -d/ -f3 || true)
|
||||
if [ -n "$TOKEN_GB" ]; then
|
||||
GB_HTML=$(curl -s "$BASE/order/$TOKEN_GB")
|
||||
# €465.54 goods (green net) + €55 world shipping = €520.54
|
||||
if printf '%s' "$CA_HTML" | grep -qF '€520.54'; then
|
||||
if printf '%s' "$GB_HTML" | grep -qF '€520.54'; then
|
||||
ok "export order total is ex-VAT + world shipping"
|
||||
else
|
||||
bad "export order total" "€520.54 not on the page"
|
||||
fi
|
||||
if printf '%s' "$CA_HTML" | grep -qF 'Zero-rated export'; then
|
||||
if printf '%s' "$GB_HTML" | grep -qF 'Zero-rated export'; then
|
||||
ok "export order states the VAT treatment"
|
||||
else
|
||||
bad "export VAT copy" "missing zero-rated export note"
|
||||
fi
|
||||
if printf '%s' "$CA_HTML" | grep -qE '≈ CA\$[0-9]+'; then
|
||||
ok "export order shows the indicative CAD amount"
|
||||
if printf '%s' "$GB_HTML" | grep -qE '≈ £[0-9]+'; then
|
||||
ok "export order shows the indicative GBP amount"
|
||||
else
|
||||
# Rates are optional by design; their absence must not fail the file
|
||||
# check, but in this repo rates.json is committed so it must appear.
|
||||
bad "indicative currency" "no ≈ CA\$ line on the CA order page"
|
||||
bad "indicative currency" "no ≈ £ line on the GB order page"
|
||||
fi
|
||||
if printf '%s' "$CA_HTML" | grep -qF 'indicative'; then
|
||||
if printf '%s' "$GB_HTML" | grep -qF 'indicative'; then
|
||||
ok "conversion is labelled indicative"
|
||||
else
|
||||
bad "indicative label" "the conversion is not labelled indicative"
|
||||
fi
|
||||
else
|
||||
bad "CA checkout" "no token from Location: $LOC_CA"
|
||||
bad "GB checkout" "no token from Location: $LOC_GB"
|
||||
fi
|
||||
|
||||
# Destinations the shop refuses. A well-formed, real address in either country
|
||||
# must not produce an order, a payment link or a redirect — the 422 is the whole
|
||||
# point, and the page has to say why rather than failing silently.
|
||||
# A two-unit white export order: unit €665, line €1330, net from the LINE
|
||||
# total (not per unit) = €1082.45, plus €55 world shipping = €1137.45.
|
||||
LOC_W=$(curl -s -o /dev/null -w '%{redirect_url}' -X POST \
|
||||
-d 'email=w%40example.org&name=W&street=X%201&postal=1&city=Y&country=CA&color=white&quantity=2' \
|
||||
-d 'email=w%40example.org&name=W&street=X%201&postal=1&city=Y&country=GB&color=white&quantity=2' \
|
||||
"$BASE/shop/fp6-pmos")
|
||||
TOKEN_W=$(printf '%s' "$LOC_W" | grep -oE '/order/[0-9a-f]{32}$' | cut -d/ -f3 || true)
|
||||
if [ -n "$TOKEN_W" ]; then
|
||||
|
|
@ -774,7 +886,56 @@ else
|
|||
bad "bulk order" "quantity=9 did not create an order: $LOC_9"
|
||||
fi
|
||||
body_has /shop/fp6-pmos 'type="number"' "quantity is a number input, not a dropdown"
|
||||
body_has /shop/fp6-pmos 'max="99"' "quantity input carries the technical ceiling"
|
||||
# The ceiling is physical now: the heaviest band any destination offers
|
||||
# (10 kg in the fixture) divided by the boxed unit weight (700 g) = 14. The
|
||||
# input advertises the BEST case across destinations; the per-country limit is
|
||||
# enforced on submit, below.
|
||||
body_has /shop/fp6-pmos 'max="14"' "quantity input carries the one-parcel ceiling"
|
||||
|
||||
# One order is one parcel. Fifteen units is 10.5 kg, past every band the
|
||||
# fixture has, so it must be refused rather than quoted a rate the carrier
|
||||
# would not accept — and the refusal has to say what WOULD fit, or the buyer is
|
||||
# left guessing.
|
||||
HEAVY_HTML=$(curl -s -X POST -d "$GOOD&quantity=15" "$BASE/shop/fp6-pmos")
|
||||
status /shop/fp6-pmos 422 POST "$GOOD&quantity=15"
|
||||
if printf '%s' "$HEAVY_HTML" | grep -qF 'up to 14 per order'; then
|
||||
ok "an over-weight order is refused with the quantity that fits"
|
||||
else
|
||||
bad "too-heavy refusal" "no 'up to 14 per order' in the response"
|
||||
fi
|
||||
if printf '%s' "$HEAVY_HTML" | grep -qF 'orders@catcrafts.net'; then
|
||||
ok "the too-heavy refusal offers a way to order anyway"
|
||||
else
|
||||
bad "too-heavy refusal" "no contact address in the response"
|
||||
fi
|
||||
|
||||
# A destination the carrier has no rate for. Since the zone fallback went away
|
||||
# there is no price to invent, so this is a refusal — and specifically NOT the
|
||||
# no-sale refusal, which is a different (policy) reason with different wording.
|
||||
AU_ADDR='email=au%40example.org&name=Alex&street=1%20George%20St&postal=2000&city=Sydney&country=AU'
|
||||
AU_HTML=$(curl -s -X POST -d "$AU_ADDR" "$BASE/shop/fp6-pmos")
|
||||
status /shop/fp6-pmos 422 POST "$AU_ADDR"
|
||||
if printf '%s' "$AU_HTML" | grep -qF 'No carrier rate for AU'; then
|
||||
ok "an uncovered destination is refused, naming the country"
|
||||
else
|
||||
bad "uncovered destination" "no carrier-rate refusal for AU in the response"
|
||||
fi
|
||||
# ...and specifically the country FIELD ERROR must be the carrier message, not
|
||||
# the no-sale one. Matched on the error markup rather than the bare sentence:
|
||||
# the no-sale line is standing copy above every buy form, so its mere presence
|
||||
# on the page says nothing about why this order was refused.
|
||||
if printf '%s' "$AU_HTML" | grep -qF 'field__error">Catcrafts does not sell'; then
|
||||
bad "uncovered destination" "AU was refused as a policy no-sale, not a missing rate"
|
||||
else
|
||||
ok "an uncovered destination is not confused with a refused one"
|
||||
fi
|
||||
AU_ORDERS=$(wc -l < "$ORDERS" 2>/dev/null || echo 0)
|
||||
curl -s -o /dev/null -X POST -d "$AU_ADDR" "$BASE/shop/fp6-pmos"
|
||||
if [ "$(wc -l < "$ORDERS" 2>/dev/null || echo 0)" = "$AU_ORDERS" ]; then
|
||||
ok "a refused destination writes no order"
|
||||
else
|
||||
bad "uncovered destination" "an order was stored for a country with no rate"
|
||||
fi
|
||||
|
||||
# No invoice exists before the money does — awaiting orders answer 404.
|
||||
status "/order/$TOKEN/invoice.md" 404
|
||||
|
|
@ -880,19 +1041,23 @@ fi
|
|||
|
||||
echo "== the confirmation email =="
|
||||
# Every paid order gets exactly one confirmation with the signed invoice
|
||||
# attached. Four orders were paid above; the mailer sweeps the ledger every
|
||||
# 2 s, so all four messages should exist within a few sweeps.
|
||||
# attached. The expected count comes from the LEDGER rather than a number
|
||||
# written here: "one per paid order" is the actual property, and a literal
|
||||
# would have to be edited by anyone who adds an order above — which is a test
|
||||
# that fails for the wrong reason and gets bumped without being read. The
|
||||
# mailer sweeps every 2 s, so the messages land within a few sweeps.
|
||||
n_paid=$(grep -c '"status":"paid"' "$ORDERS" || true)
|
||||
i=0
|
||||
until [ "$(ls "$WORK"/mail-*.eml 2>/dev/null | wc -l)" -ge 4 ]; do
|
||||
until [ "$(ls "$WORK"/mail-*.eml 2>/dev/null | wc -l)" -ge "$n_paid" ]; do
|
||||
i=$((i + 1))
|
||||
if [ "$i" -gt 60 ]; then break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
n_mail=$(ls "$WORK"/mail-*.eml 2>/dev/null | wc -l)
|
||||
if [ "$n_mail" -eq 4 ]; then
|
||||
if [ "$n_mail" -eq "$n_paid" ]; then
|
||||
ok "one confirmation email per paid order ($n_mail sent)"
|
||||
else
|
||||
bad "confirmation email count" "expected 4, got $n_mail"
|
||||
bad "confirmation email count" "expected $n_paid, got $n_mail"
|
||||
fi
|
||||
|
||||
# The NL order's message, found by its own order link (the same email address
|
||||
|
|
@ -921,11 +1086,11 @@ else
|
|||
fi
|
||||
|
||||
# The export order's message states the VAT treatment its invoice carries.
|
||||
MAIL_CA=$(grep -l "/order/$TOKEN_CA" "$WORK"/mail-*.eml 2>/dev/null | head -n1)
|
||||
if [ -n "$MAIL_CA" ] && grep -qF 'zero-rated export' "$MAIL_CA"; then
|
||||
MAIL_GB=$(grep -l "/order/$TOKEN_GB" "$WORK"/mail-*.eml 2>/dev/null | head -n1)
|
||||
if [ -n "$MAIL_GB" ] && grep -qF 'zero-rated export' "$MAIL_GB"; then
|
||||
ok "export confirmation states the zero-rated treatment"
|
||||
else
|
||||
bad "export confirmation" "no message for the CA order, or no VAT note in it"
|
||||
bad "export confirmation" "no message for the GB order, or no VAT note in it"
|
||||
fi
|
||||
|
||||
# Idempotency comes from the ledger's notified event, not from luck in
|
||||
|
|
@ -962,6 +1127,25 @@ status /shop/fp6-pmos 422 POST 'name=Ada&street=x&postal=1&city=y&country=NL'
|
|||
status /shop/fp6-pmos 422 POST 'email=nonsense&'"$GOOD" # bad email (dup field keeps first)
|
||||
status /shop/fp6-pmos 422 POST 'email=a%40b.example&country=NL' # missing address
|
||||
status /shop/fp6-pmos 422 POST "$GOOD&website=spam" # honeypot
|
||||
# Destinations the shop refuses (Money::NoSaleCountries). Well-formed, real
|
||||
# addresses: the refusal is policy, not a shape check, so it has to hold for
|
||||
# every spelling the form accepts. Deliberately outside the SHOP_OPEN gate —
|
||||
# validation runs before the coming-soon check, so this must answer 422 whether
|
||||
# the shop is open or not, and it is the assertion that would catch the block
|
||||
# being lost in a refactor.
|
||||
US_ADDR='email=us%40example.org&name=Pat&street=1%20Main%20St&postal=43004&city=Columbus&country=US'
|
||||
ORDERS_BEFORE=$(wc -l < "$ORDERS" 2>/dev/null || echo 0)
|
||||
status /shop/fp6-pmos 422 POST "$US_ADDR" # United States
|
||||
status /shop/fp6-pmos 422 POST 'email=ca%40example.org&name=Terry&street=1%20Bloor%20St&postal=M4W&city=Toronto&country=CA'
|
||||
status /shop/fp6-pmos 422 POST 'email=us%40example.org&name=Pat&street=1%20Main%20St&postal=43004&city=Columbus&country=us'
|
||||
# Refused in validation means nothing reached the ledger and no payment link
|
||||
# was ever created.
|
||||
ORDERS_AFTER=$(wc -l < "$ORDERS" 2>/dev/null || echo 0)
|
||||
if [ "$ORDERS_BEFORE" = "$ORDERS_AFTER" ]; then
|
||||
ok "a refused destination creates no order record"
|
||||
else
|
||||
bad "refusal leaks an order" "orders went from $ORDERS_BEFORE to $ORDERS_AFTER"
|
||||
fi
|
||||
status /shop/nope 404 POST "$GOOD" # unknown product
|
||||
status /projects 405 POST 'x=1' # not a form target
|
||||
|
||||
|
|
@ -984,6 +1168,26 @@ if grep -qF 'field__error' "$WORK/rejected.html"; then
|
|||
else
|
||||
bad "rejected form error" "no .field__error in the response"
|
||||
fi
|
||||
# A refused destination says why, in the form, with the address still in it —
|
||||
# the visitor should learn the shop does not sell there, not that something
|
||||
# went wrong.
|
||||
curl -s -X POST -d "$US_ADDR" "$BASE/shop/fp6-pmos" > "$WORK/refused.html"
|
||||
if grep -qF 'does not sell or ship to the United States or Canada' "$WORK/refused.html"; then
|
||||
ok "refusal explains itself on the form"
|
||||
else
|
||||
bad "refusal copy" "the US/CA refusal message is not on the re-rendered page"
|
||||
fi
|
||||
if grep -qF 'value="Pat"' "$WORK/refused.html"; then
|
||||
ok "a refused submission keeps what was typed"
|
||||
else
|
||||
bad "refusal echo" "form values were dropped on refusal"
|
||||
fi
|
||||
# The buy panel warns before anyone fills it in, and the preview script carries
|
||||
# the same list so it cannot quote a total the server would refuse.
|
||||
body_has /shop/fp6-pmos 'does not sell or ship to the United States or Canada' \
|
||||
"buy panel states where the shop does not sell"
|
||||
body_has /shop/fp6-pmos '"x":["US","CA"]' \
|
||||
"total preview knows the refused destinations"
|
||||
# The honeypot message must not name the trap, or it teaches the next bot.
|
||||
# Only the ERROR NOTICE is inspected: the re-rendered form legitimately
|
||||
# contains the name="website" field itself — that IS the trap, re-armed.
|
||||
|
|
|
|||
Loading…
Reference in a new issue