merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

test_merchant_depositcheck_confirmation.sh (11354B)


      1 #!/usr/bin/env bash
      2 # This file is part of TALER
      3 # Copyright (C) 2026 Taler Systems SA
      4 #
      5 # TALER is free software; you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as
      7 # published by the Free Software Foundation; either version 3, or
      8 # (at your option) any later version.
      9 #
     10 # TALER is distributed in the hope that it will be useful, but
     11 # WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public
     16 # License along with TALER; see the file COPYING.  If not, see
     17 # <http://www.gnu.org/licenses/>
     18 #
     19 
     20 
     21 
     22 
     23 # Testcase for the depositcheck/reconciliation confirmation race:
     24 # taler-merchant-depositcheck creates merchant_expected_transfers with
     25 # expected_credit_amount = NULL because /deposits only knows the coin's
     26 # contribution.  taler-merchant-reconciliation later fills in the total
     27 # amount via /transfers/$WTID.  The confirmation linking must also happen
     28 # on that UPDATE, not only on INSERT.
     29 
     30 # FIXME: we need to make this tests simpler
     31 #
     32 # goto REPRODUCE THE RACE
     33 # I was unable to test by endpoint because of src/backend/taler-merchant-httpd_get-private-transfers.c:84 is hardcoded with true
     34 
     35 set -eu
     36 
     37 . setup.sh
     38 
     39 # Exit, with status code "skip" (no 'real' failure)
     40 function exit_skip() {
     41     echo "SKIPPING: $1"
     42     exit 77
     43 }
     44 
     45 # Use fakebank (instant) for this test.
     46 USE_FAKEBANK=1
     47 ACCOUNT="exchange-account-2"
     48 WIRE_METHOD="x-taler-bank"
     49 BANK_FLAGS="-f -d $WIRE_METHOD -u $ACCOUNT"
     50 BANK_URL="http://localhost:8082/"
     51 
     52 
     53 echo -n "Testing for taler-harness"
     54 taler-harness --help >/dev/null </dev/null || exit_skip " MISSING"
     55 echo " FOUND"
     56 
     57 # Launch exchange, merchant and bank.
     58 setup -c "test_template.conf" \
     59       -em \
     60       -r "merchant-exchange-default" \
     61       $BANK_FLAGS
     62 LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX)
     63 CONF="test_template.conf.edited"
     64 WALLET_DB=$(mktemp -p "${TMPDIR:-/tmp}" test_wallet.json-XXXXXX)
     65 EXCHANGE_URL="http://localhost:8081/"
     66 
     67 FACADE_URL="http://localhost:8082/accounts/gnunet/taler-revenue/"
     68 FACADE_USERNAME="gnunet"
     69 FACADE_PASSWORD="password"
     70 
     71 # Extract merchant DB connection for direct DB assertions.
     72 echo -n "Checking for psql ..."
     73 psql --version >/dev/null 2>&1 || exit_skip " no psql"
     74 echo " OK"
     75 
     76 DB_URI=$(sed -n '/^\[merchantdb-postgres\]/,/^\[/p' "$CONF" \
     77   | awk -F= '/^CONFIG[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2; exit }')
     78 if [ -z "$DB_URI" ]
     79 then
     80     exit_fail "Could not extract merchant DB URI from $CONF"
     81 fi
     82 
     83 # Helper to run a query against a merchant instance schema.
     84 function merchant_psql() {
     85     psql -At -q -d "$DB_URI" -v ON_ERROR_STOP=1 \
     86          -c "SET search_path TO ${MERCHANT_SCHEMA}; $1"
     87 }
     88 
     89 echo -n "First prepare wallet with coins..."
     90 rm -f "${WALLET_DB}"
     91 taler-wallet-cli \
     92     --no-throttle \
     93     --wallet-db="$WALLET_DB" \
     94     api \
     95     --expect-success 'withdrawTestBalance' \
     96   "$(jq -n '
     97     {
     98         amount: "TESTKUDOS:99",
     99         corebankApiBaseUrl: $BANK_URL,
    100         exchangeBaseUrl: $EXCHANGE_URL
    101     }' \
    102     --arg BANK_URL "${BANK_URL}" \
    103     --arg EXCHANGE_URL "$EXCHANGE_URL"
    104   )" 2>wallet-withdraw-1.err >wallet-withdraw-1.out
    105 echo -n "."
    106 taler-exchange-wirewatch \
    107     -L "INFO" \
    108     -c "$CONF" \
    109     -t \
    110     &> taler-exchange-wirewatch.out
    111 echo -n "."
    112 timeout 60 taler-wallet-cli \
    113     --wallet-db="$WALLET_DB" \
    114     run-until-done \
    115     2>wallet-withdraw-finish-1.err \
    116     >wallet-withdraw-finish-1.out
    117 echo " OK"
    118 
    119 #
    120 # CREATE INSTANCE FOR TESTING
    121 #
    122 
    123 echo -n "Configuring merchant admin instance ..."
    124 GNUNET_PAYTO="payto://x-taler-bank/localhost/gnunet?receiver-name=gnunet"
    125 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    126     http://localhost:9966/management/instances \
    127     -d '{"auth":{"method":"token","password":"super_secret"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \
    128     -w "%{http_code}" -s -o /dev/null)
    129 
    130 if [ "$STATUS" != "204" ]
    131 then
    132     exit_fail "Expected 204 no content. Got: $STATUS"
    133 fi
    134 ADMIN_TOKEN=$(merchant_login admin super_secret)
    135 echo "OK"
    136 
    137 # Resolve the per-instance schema name for direct DB inspection.
    138 MERCHANT_SERIAL=$(psql -At -q -d "$DB_URI" -v ON_ERROR_STOP=1 \
    139   -c "SELECT merchant_serial FROM merchant.merchant_instances WHERE merchant_id='admin';")
    140 MERCHANT_SCHEMA="merchant_instance_${MERCHANT_SERIAL}"
    141 
    142 echo -n "Configuring bank account..."
    143 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    144     -H "Authorization: Bearer $ADMIN_TOKEN" \
    145     http://localhost:9966/private/accounts \
    146     -d '{"payto_uri":"'"$GNUNET_PAYTO"'","credit_facade_url":"'"${FACADE_URL}"'","credit_facade_credentials":{"type":"basic","username":"'"$FACADE_USERNAME"'","password":"'"$FACADE_PASSWORD"'"}}' \
    147     -w "%{http_code}" -s -o /dev/null)
    148 
    149 if [ "$STATUS" != "200" ]
    150 then
    151     exit_fail "Expected 200 OK. Got: $STATUS"
    152 fi
    153 echo "OK"
    154 
    155 # CREATE ORDER AND SELL IT
    156 echo -n "Creating order to be paid..."
    157 STATUS=$(curl 'http://localhost:9966/private/orders' \
    158     -H "Authorization: Bearer $ADMIN_TOKEN" \
    159     -d '{"refund_delay":{"d_us":0},"order":{"amount":"TESTKUDOS:1","summary":"payme"}}' \
    160     -w "%{http_code}" \
    161     -s \
    162     -o "$LAST_RESPONSE")
    163 
    164 if [ "$STATUS" != "200" ]
    165 then
    166     exit_fail "Expected 200 OK. Got: $STATUS " "$(cat "$LAST_RESPONSE")"
    167 fi
    168 
    169 ORDER_ID=$(jq -e -r .order_id < "$LAST_RESPONSE")
    170 STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \
    171               -H "Authorization: Bearer $ADMIN_TOKEN" \
    172               -w "%{http_code}" \
    173               -s \
    174               -o "$LAST_RESPONSE")
    175 
    176 if [ "$STATUS" != "200" ]
    177 then
    178     exit_fail "Expected 200 ok. Got: $STATUS" "$(cat "$LAST_RESPONSE")"
    179 fi
    180 PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE")
    181 echo OK
    182 
    183 NOW=$(date +%s)
    184 echo -n "Pay order ..."
    185 taler-wallet-cli \
    186     --no-throttle \
    187     --wallet-db="$WALLET_DB" \
    188     handle-uri "${PAY_URL}" \
    189     -y 2> wallet-pay1.err > wallet-pay1.log
    190 NOW2=$(date +%s)
    191 echo "OK. Took $(( NOW2 -  NOW))s."
    192 
    193 STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \
    194               -H "Authorization: Bearer $ADMIN_TOKEN" \
    195               -w "%{http_code}" \
    196               -s \
    197               -o "$LAST_RESPONSE")
    198 
    199 if [ "$STATUS" != "200" ]
    200 then
    201     exit_fail "Expected 200 Ok. Got: $STATUS" "$(cat "$LAST_RESPONSE")"
    202 fi
    203 
    204 ORDER_STATUS=$(jq -r .order_status < "$LAST_RESPONSE")
    205 if [ "$ORDER_STATUS" != "paid" ]
    206 then
    207     exit_fail "Expected order status 'paid'. Got: $ORDER_STATUS" "$(cat "$LAST_RESPONSE")"
    208 fi
    209 
    210 #
    211 # WIRE TRANSFER TO MERCHANT
    212 #
    213 
    214 WIRE_DEADLINE=$(jq -r .contract_terms.wire_transfer_deadline.t_s < "$LAST_RESPONSE")
    215 NOW=$(date +%s)
    216 
    217 TO_SLEEP="$(( 3600 + WIRE_DEADLINE - NOW ))"
    218 echo -n "Perform wire transfers (with ${TO_SLEEP}s timeshift) ..."
    219 taler-exchange-aggregator \
    220     -y \
    221     -c "$CONF" \
    222     -T "${TO_SLEEP}000000" \
    223     -t \
    224     -L INFO &> aggregator.log
    225 taler-exchange-transfer \
    226     -c "$CONF" \
    227     -t \
    228     -L INFO &> transfer.log
    229 echo " DONE"
    230 
    231 #
    232 # REPRODUCE THE RACE: import the bank transfer first, then run depositcheck,
    233 # then reconciliation.  With the bug, the transfer stays unconfirmed because
    234 # the expected_credit_amount only becomes known on UPDATE.
    235 #
    236 
    237 echo -n "Running taler-merchant-wirewatch to import transfer ..."
    238 taler-merchant-wirewatch \
    239     -c "$CONF" \
    240     -t \
    241     -L INFO &> merchant-wirewatch.log
    242 echo " OK"
    243 
    244 # DB sanity check: we have the bank-side row but no expected transfer yet.
    245 echo -n "Checking DB state after wirewatch ..."
    246 DB_STATE=$(merchant_psql "SELECT (SELECT COUNT(*) FROM merchant_transfers), (SELECT COUNT(*) FROM merchant_expected_transfers);")
    247 IFS='|' read -r TRANSFER_COUNT EXPECTED_COUNT <<< "$DB_STATE"
    248 if [ "$TRANSFER_COUNT" != "1" ]
    249 then
    250     exit_fail "Expected 1 merchant_transfers row, got $TRANSFER_COUNT"
    251 fi
    252 if [ "$EXPECTED_COUNT" != "0" ]
    253 then
    254     exit_fail "Expected 0 merchant_expected_transfers rows before depositcheck, got $EXPECTED_COUNT"
    255 fi
    256 echo " OK"
    257 
    258 echo -n "Running taler-merchant-depositcheck to create expected transfer ..."
    259 taler-merchant-depositcheck \
    260     -c "$CONF" \
    261     -e "http://localhost:8081/" \
    262     -T "${TO_SLEEP}000000" \
    263     -t \
    264     -L INFO &> merchant-depositcheck.log
    265 echo " OK"
    266 
    267 # DB sanity check: depositcheck created an unconfirmed expected transfer
    268 # without knowing the final credit amount.
    269 echo -n "Checking DB state after depositcheck ..."
    270 DB_STATE=$(merchant_psql "SELECT expected_credit_amount IS NULL, confirmed, retry_needed FROM merchant_expected_transfers;")
    271 IFS='|' read -r AMOUNT_NULL CONFIRMED RETRY <<< "$DB_STATE"
    272 if [ "$AMOUNT_NULL" != "t" ]
    273 then
    274     exit_fail "Expected expected_credit_amount to be NULL after depositcheck"
    275 fi
    276 if [ "$CONFIRMED" != "f" ]
    277 then
    278     exit_fail "Expected confirmed=false after depositcheck, got $CONFIRMED"
    279 fi
    280 if [ "$RETRY" != "t" ]
    281 then
    282     exit_fail "Expected retry_needed=true after depositcheck, got $RETRY"
    283 fi
    284 echo " OK"
    285 
    286 echo -n "Running taler-merchant-reconciliation to finalize transfer amount ..."
    287 taler-merchant-reconciliation \
    288     -c "$CONF" \
    289     -T "${TO_SLEEP}000000" \
    290     -t \
    291     -L INFO &> merchant-reconciliation.log
    292 echo " OK"
    293 
    294 # Primary regression check: reconciliation must have linked the expected
    295 # transfer to the imported bank transfer now that the amount is known.
    296 echo -n "Checking DB state after reconciliation ..."
    297 DB_STATE=$(merchant_psql "SELECT expected_credit_amount IS NULL, confirmed FROM merchant_expected_transfers;")
    298 IFS='|' read -r AMOUNT_NULL CONFIRMED <<< "$DB_STATE"
    299 if [ "$AMOUNT_NULL" != "f" ]
    300 then
    301     exit_fail "Expected expected_credit_amount to be set after reconciliation"
    302 fi
    303 if [ "$CONFIRMED" != "t" ]
    304 then
    305     exit_fail "Expected confirmed=true after reconciliation, got $CONFIRMED (UPDATE trigger likely missing)"
    306 fi
    307 echo " OK"
    308 
    309 echo -n "Checking incoming transfer is confirmed via API ..."
    310 STATUS=$(curl 'http://localhost:9966/private/incoming' \
    311               -H "Authorization: Bearer $ADMIN_TOKEN" \
    312               -w "%{http_code}" \
    313               -s \
    314               -o "$LAST_RESPONSE")
    315 if [ "$STATUS" != "200" ]
    316 then
    317     exit_fail "Expected response 200 Ok. Got: $STATUS" "$(jq . < "$LAST_RESPONSE")"
    318 fi
    319 
    320 INCOMING_LIST_SIZE=$(jq -r '.incoming | length' < "$LAST_RESPONSE")
    321 if [ "$INCOMING_LIST_SIZE" != "1" ]
    322 then
    323     exit_fail "Expected one incoming transfer. Got: $INCOMING_LIST_SIZE" "$(jq . < "$LAST_RESPONSE")"
    324 fi
    325 
    326 CONFIRMED=$(jq -r '.incoming[0].confirmed' < "$LAST_RESPONSE")
    327 if [ "$CONFIRMED" != "true" ]
    328 then
    329     jq . < "$LAST_RESPONSE"
    330     exit_fail "Expected incoming[0].confirmed 'true'. Got: $CONFIRMED"
    331 fi
    332 echo " OK"
    333 
    334 # Also verify the order is wired.
    335 echo -n "Checking order status ..."
    336 STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}?transfer=YES" \
    337               -H "Authorization: Bearer $ADMIN_TOKEN" \
    338               -w "%{http_code}" \
    339               -s \
    340               -o "$LAST_RESPONSE")
    341 
    342 if [ "$STATUS" != "200" ]
    343 then
    344     jq . < "$LAST_RESPONSE"
    345     exit_fail "Expected 200 ok. got: $STATUS" "$(cat "$LAST_RESPONSE")"
    346 fi
    347 
    348 WAS_WIRED=$(jq -r .wired < "$LAST_RESPONSE")
    349 if [ "$WAS_WIRED" != "true" ]
    350 then
    351     jq . < "$LAST_RESPONSE"
    352     exit_fail "Got .wired 'false', expected 'true'"
    353 fi
    354 echo " OK"
    355 
    356 echo "TEST PASSED"
    357 
    358 exit 0