#!/usr/bin/env bash # Offline tests for the write skill — everything runs against # tests/stub-dol-write.sh via the DOL_WRITE env hook, so NOTHING is written to # the sandbox or prod (zero credentials, zero network). # # payment-record.sh (the erp#37 varchar(50) fix): # 1. Long Qonto id → POST carries the UUID suffix; JSON reports it; stderr says so. # 2. Wise numeric id → passes through untouched, no normalization notice. # 3. Id still >50 chars after normalization → refused BEFORE any POST, error # cites varchar(50) (never a silent truncation). # client-dossier ops (erp#65 phase 1): # 4. thirdparty-update.sh refuses a non-allowlisted field (code_client) BEFORE # any PUT, naming the offender. # 5. contact-create.sh dedupes on a case-insensitive email match → no POST, # {"deduped": true}; and refuses the WIP payloads' `soc2` (→ `poste`). # 6. happy path: thirdparty_update + contact through promote-apply # --target sandbox (stubbed); a second apply is a proven no-op # (changed=0 for the fiche, deduped=true for the contact). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PR="${SCRIPT_DIR}/../scripts/payment-record.sh" TU="${SCRIPT_DIR}/../scripts/thirdparty-update.sh" CC="${SCRIPT_DIR}/../scripts/contact-create.sh" PA="${SCRIPT_DIR}/../scripts/promote-apply.sh" PP="${SCRIPT_DIR}/../scripts/promote-plan.sh" STUB="${SCRIPT_DIR}/stub-dol-write.sh" fail() { echo "FAIL: $*" >&2; exit 1; } bash -n "${PR}" || fail "bash -n payment-record.sh" bash -n "${TU}" || fail "bash -n thirdparty-update.sh" bash -n "${CC}" || fail "bash -n contact-create.sh" bash -n "${PA}" || fail "bash -n promote-apply.sh" bash -n "${PP}" || fail "bash -n promote-plan.sh" bash -n "${STUB}" || fail "bash -n stub-dol-write.sh" STATE="$(mktemp -d -t prtest.XXXXXX)" trap 'rm -rf "${STATE}"' EXIT LONG="arcodange-1246-1-transaction-019f14c5-e254-7ac9-9e9f-307ed9-d55f44" SHORT="019f14c5-e254-7ac9-9e9f-307ed9-d55f44" # --- Case 1: long Qonto id is normalized to the UUID suffix --- OUT="$(printf '{"invoice_id":13,"kind":"supplier","account_id":1,"amount":96,"transaction_id":"%s"}' "${LONG}" \ | DOL_WRITE="${STUB}" STUB_STATE="${STATE}" bash "${PR}" 2>"${STATE}/stderr1")" \ || fail "long-qonto-id: expected success, got $?" grep -q "\"num_payment\": \"${SHORT}\"" "${STATE}/post_body.json" \ || fail "long-qonto-id: POST body must carry the SHORT num, got: $(cat "${STATE}/post_body.json")" python3 -c " import json, sys o = json.loads('''${OUT}''') assert o['transaction_id'] == '${SHORT}', o assert o['id'] == 77 and o['bank_transaction_id'] == 556, o " || fail "long-qonto-id: output JSON must report the normalized num, got: ${OUT}" grep -q 'normalized' "${STATE}/stderr1" || fail "long-qonto-id: normalization must be announced on stderr" # --- Case 2: Wise numeric id passes through unchanged --- OUT="$(printf '{"invoice_id":19,"account_id":2,"transaction_id":"2159468139"}' \ | DOL_WRITE="${STUB}" STUB_STATE="${STATE}" bash "${PR}" 2>"${STATE}/stderr2")" \ || fail "wise-id: expected success, got $?" grep -q '"num_payment": "2159468139"' "${STATE}/post_body.json" \ || fail "wise-id: POST body must carry the id untouched" grep -q 'normalized' "${STATE}/stderr2" && fail "wise-id: must NOT announce a normalization" # --- Case 3: >50 chars after normalization is refused before any POST --- rm -f "${STATE}/post_body.json" "${STATE}/post_endpoint" BAD="qonto-migration-batch-7-payment-reference-0123456789-0123456789" # 63 chars, no "transaction-" rc=0 printf '{"invoice_id":13,"kind":"supplier","account_id":1,"amount":96,"transaction_id":"%s"}' "${BAD}" \ | DOL_WRITE="${STUB}" STUB_STATE="${STATE}" bash "${PR}" >/dev/null 2>"${STATE}/stderr3" || rc=$? [[ "${rc}" -ne 0 ]] || fail "overlong-id: must exit non-zero" [[ ! -f "${STATE}/post_body.json" ]] || fail "overlong-id: must refuse BEFORE any POST" grep -q 'varchar(50)' "${STATE}/stderr3" || fail "overlong-id: error must cite the varchar(50) constraint" echo "OK: payment-record normalization tests passed (long→short, wise untouched, >50 refused pre-POST)" # --- Case 4: thirdparty-update refuses a non-allowlisted field pre-PUT --- S4="$(mktemp -d -t tutest.XXXXXX)"; trap 'rm -rf "${STATE}" "${S4}"' EXIT rc=0 printf '{"name":"KissMetrics","code_client":"CL9999"}' \ | DOL_WRITE="${STUB}" STUB_STATE="${S4}" bash "${TU}" 1 - >/dev/null 2>"${S4}/stderr" || rc=$? [[ "${rc}" -ne 0 ]] || fail "allowlist: payload with code_client must be refused (exit non-zero)" [[ ! -f "${S4}/put_body.json" ]] || fail "allowlist: refusal must happen BEFORE any PUT" grep -q 'code_client' "${S4}/stderr" || fail "allowlist: the error must name the offending field" grep -qi 'allowlist' "${S4}/stderr" || fail "allowlist: the error must say it is an allowlist refusal" echo "OK: thirdparty-update allowlist — code_client refused pre-PUT, offender named" # --- Case 5: contact-create dedupes by case-insensitive email → no POST --- S5="$(mktemp -d -t cctest.XXXXXX)"; trap 'rm -rf "${STATE}" "${S4}" "${S5}"' EXIT printf '%s' '[{"id":"41","socid":"1","lastname":"ROOTERING","firstname":"hendrik","poste":"COO","email":"Hendrik@KissMetrics.IO"}]' \ > "${S5}/contacts.json" OUT="$(printf '{"socid":"1","lastname":"Rootering","firstname":"Hendrik","poste":"COO","email":"hendrik@kissmetrics.io"}' \ | DOL_WRITE="${STUB}" STUB_STATE="${S5}" bash "${CC}" 2>/dev/null)" \ || fail "contact-dedupe: expected success, got $?" python3 -c " import json o = json.loads('''${OUT}''') assert o == {'id': 41, 'deduped': True}, o " || fail "contact-dedupe: must return the existing id with deduped:true, got: ${OUT}" [[ ! -f "${S5}/contact_post_body.json" ]] || fail "contact-dedupe: must NOT POST when a match exists" # 5b — the WIP payloads' soc2 is not a Dolibarr field: refuse, point to poste rc=0 printf '{"socid":"1","lastname":"Rootering","soc2":"COO"}' \ | DOL_WRITE="${STUB}" STUB_STATE="${S5}" bash "${CC}" >/dev/null 2>"${S5}/stderr5b" || rc=$? [[ "${rc}" -ne 0 ]] || fail "soc2: must be refused (exit non-zero)" grep -q 'poste' "${S5}/stderr5b" || fail "soc2: the error must point to 'poste'" [[ ! -f "${S5}/contact_post_body.json" ]] || fail "soc2: refusal must happen BEFORE any POST" echo "OK: contact-create dedupe — email match returns existing id, no POST; soc2 refused → poste" # --- Case 6: happy path — both ops through promote-apply; re-apply is a no-op --- S6="$(mktemp -d -t patest.XXXXXX)"; trap 'rm -rf "${STATE}" "${S4}" "${S5}" "${S6}"' EXIT cat > "${S6}/manifest.json" <<'JSON' [ { "op": "thirdparty_update", "ref": "tp", "input": { "socid": 1, "fields": { "email": "evan@kissmetrics.io", "note_public": "NEW NOTE — dossier v2" } } }, { "op": "contact", "ref": "ct", "input": { "socid": "1", "lastname": "Rootering", "firstname": "Hendrik", "poste": "COO", "email": "hendrik@kissmetrics.io" } } ] JSON bash "${PP}" "${S6}/manifest.json" >/dev/null || fail "promote-plan: must render the new op kinds" OUT1="$(DOL_WRITE="${STUB}" STUB_STATE="${S6}" bash "${PA}" "${S6}/manifest.json" --target sandbox 2>/dev/null)" \ || fail "promote-apply run 1: expected success, got $?" grep -q 'thirdparty_update' <<<"${OUT1}" || fail "run 1: thirdparty_update op must be reported" grep -q 'changed=2' <<<"${OUT1}" || fail "run 1: both fields must read back as changed, got: ${OUT1}" grep -q 'deduped' <<<"${OUT1}" && fail "run 1: nothing must dedupe on a fresh target" grep -q -- '-> id=88' <<<"${OUT1}" || fail "run 1: contact must be created (id 88), got: ${OUT1}" grep -q '"note_public": "NEW NOTE — dossier v2"' "${S6}/put_body.json" \ || fail "run 1: PUT body must carry the new note, got: $(cat "${S6}/put_body.json")" grep -q '"poste": "COO"' "${S6}/contact_post_body.json" \ || fail "run 1: contact POST must carry poste=COO, got: $(cat "${S6}/contact_post_body.json")" rm -f "${S6}/contact_post_body.json" OUT2="$(DOL_WRITE="${STUB}" STUB_STATE="${S6}" bash "${PA}" "${S6}/manifest.json" --target sandbox 2>/dev/null)" \ || fail "promote-apply run 2: expected success, got $?" grep -q 'changed=0 (no-op)' <<<"${OUT2}" || fail "run 2: thirdparty_update must be a no-op, got: ${OUT2}" grep -q 'deduped=true' <<<"${OUT2}" || fail "run 2: contact must dedupe, got: ${OUT2}" grep -q -- '-> id=88' <<<"${OUT2}" || fail "run 2: dedupe must return the run-1 id" [[ ! -f "${S6}/contact_post_body.json" ]] || fail "run 2: must NOT POST a duplicate contact" echo "OK: promote-apply happy path — run 1 applies (changed=2, contact id 88), run 2 is a no-op (changed=0, deduped)" echo "OK: all offline tests passed"