test_merchant_fountains.sh (37743B)
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 # Exercises the token fountain endpoints (DD 98): private CRUD, 21 # GET /fountain/info, withdrawal validation and replay after family deletion. 22 # Unblinding and spending withdrawn tokens are exercised by the C harness. 23 24 # Cleanup to run whenever we exit 25 function my_cleanup() 26 { 27 for n in $(jobs -p) 28 do 29 kill "$n" 2> /dev/null || true 30 done 31 wait 32 if [ -n "${LAST_RESPONSE+x}" ] 33 then 34 rm -f "${LAST_RESPONSE}" 35 fi 36 } 37 38 . setup.sh 39 40 setup -c test_template.conf -m 41 CONF="test_template.conf.edited" 42 LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) 43 MERCHANT_URL="http://localhost:9966" 44 45 echo -n "Configuring 'admin' instance ..." >&2 46 47 STATUS=$(curl -H "Content-Type: application/json" -X POST \ 48 "$MERCHANT_URL/management/instances" \ 49 -d '{"auth":{"method":"token","password":"new_pw"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ 50 -w "%{http_code}" \ 51 -s \ 52 -o /dev/null) 53 54 if [ "$STATUS" != "204" ] 55 then 56 exit_fail "Expected 204, instance created. got: $STATUS" >&2 57 fi 58 59 BASIC_AUTH=$(echo -n admin:new_pw | base64) 60 61 STATUS=$(curl -H "Content-Type: application/json" -X POST \ 62 -H "Authorization: Basic $BASIC_AUTH" \ 63 "$MERCHANT_URL/private/token" \ 64 -d '{"scope":"spa"}' \ 65 -w "%{http_code}" -s -o "$LAST_RESPONSE") 66 67 if [ "$STATUS" != "200" ] 68 then 69 exit_fail "Expected 200 OK. Got: $STATUS" 70 fi 71 72 BEARER_TOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") 73 74 echo " OK" >&2 75 76 # 77 # CREATE TOKEN FAMILIES FOR THE FOUNTAIN GRANTS 78 # 79 echo -n "Creating discount token family..." >&2 80 VALID_AFTER="{\"t_s\": $(date +%s)}" # now 81 VALID_BEFORE="{\"t_s\": $(date +%s -d "+300 days")}" # 300 days from now 82 DURATION="{\"d_us\": $(expr 3 \* 60 \* 1000000)}" # 3 minutes 83 GRANULARITY="{\"d_us\": $(expr 60 \* 1000000)}" # 1 minute 84 STATUS=$(curl "$MERCHANT_URL/private/tokenfamilies" \ 85 -X POST \ 86 -H "Authorization: Bearer $BEARER_TOKEN" \ 87 -d "{\"kind\": \"discount\", \"slug\":\"test-discount\", \"name\": \"Test discount\", \"description\": \"Less money\", \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $GRANULARITY}" \ 88 -w "%{http_code}" \ 89 -s \ 90 -o "$LAST_RESPONSE") 91 if [ "$STATUS" != "204" ] 92 then 93 cat "$LAST_RESPONSE" >&2 94 exit_fail "Expected '204' response. Got instead $STATUS" 95 fi 96 echo "Ok" >&2 97 98 echo -n "Creating subscription token family..." >&2 99 STATUS=$(curl "$MERCHANT_URL/private/tokenfamilies" \ 100 -X POST \ 101 -H "Authorization: Bearer $BEARER_TOKEN" \ 102 -d "{\"kind\": \"subscription\", \"slug\":\"test-subscription\", \"name\": \"Test subscription\", \"description\": \"Monthly pass\", \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $GRANULARITY}" \ 103 -w "%{http_code}" \ 104 -s \ 105 -o "$LAST_RESPONSE") 106 if [ "$STATUS" != "204" ] 107 then 108 cat "$LAST_RESPONSE" >&2 109 exit_fail "Expected '204' response. Got instead $STATUS" 110 fi 111 echo "Ok" >&2 112 113 # 114 # CREATE A FOUNTAIN 115 # 116 echo -n "Creating fountain..." >&2 117 POLL_FREQ="{\"d_us\": 3600000000}" # 1 hour 118 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 119 -X POST \ 120 -H "Authorization: Bearer $BEARER_TOKEN" \ 121 -d "{\"description\": \"campaign-reference-42\", \"poll_freq\": $POLL_FREQ, \"grants\": []}" \ 122 -w "%{http_code}" \ 123 -s \ 124 -o "$LAST_RESPONSE") 125 if [ "$STATUS" != "200" ] 126 then 127 cat "$LAST_RESPONSE" >&2 128 exit_fail "Expected '200 OK' response. Got instead $STATUS" 129 fi 130 FOUNTAIN_ID=$(jq -e -r .fountain_id < "$LAST_RESPONSE") 131 FOUNTAIN_SECRET=$(jq -e -r .fountain_secret < "$LAST_RESPONSE") 132 echo "Ok (id: $FOUNTAIN_ID)" >&2 133 134 echo -n "Empty fountain remains accessible while grants are added and cleared..." >&2 135 FOUNTAIN_GRANTS='[{"token_family_slug":"test-discount","tokens_per_period_limit":5,"tokens_per_period_stash":2,"key_window_size":2},{"token_family_slug":"test-subscription","tokens_per_period_limit":3,"tokens_per_period_stash":1,"key_window_size":1}]' 136 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 137 -H "Authorization: Bearer $FOUNTAIN_SECRET" \ 138 -w "%{http_code}" -s -o "$LAST_RESPONSE") 139 if [ "$STATUS" != "200" ] || ! jq -e '.grants == []' "$LAST_RESPONSE" > /dev/null 140 then 141 exit_fail "Expected accessible empty fountain after creation" 142 fi 143 for EXPECTED_GRANTS in '[]' "$FOUNTAIN_GRANTS" '[]' "$FOUNTAIN_GRANTS" 144 do 145 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 146 -X PATCH -H "Authorization: Bearer $BEARER_TOKEN" \ 147 -d "{\"grants\": $EXPECTED_GRANTS}" \ 148 -w "%{http_code}" -s -o "$LAST_RESPONSE") 149 if [ "$STATUS" != "204" ] 150 then 151 exit_fail "Expected 204 updating fountain grants, got $STATUS" 152 fi 153 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 154 -H "Authorization: Bearer $FOUNTAIN_SECRET" \ 155 -w "%{http_code}" -s -o "$LAST_RESPONSE") 156 if [ "$STATUS" != "200" ] || ! jq -e --argjson expected "$EXPECTED_GRANTS" \ 157 '(.grants | map(.token_family_slug) | sort) == ($expected | map(.token_family_slug) | sort)' \ 158 "$LAST_RESPONSE" > /dev/null 159 then 160 exit_fail "Fountain info did not reflect the updated grants" 161 fi 162 done 163 echo "Ok" >&2 164 165 echo -n "Patching poll frequency and preserving it when omitted..." >&2 166 for PATCH_BODY in '{"poll_freq":{"d_us":60000000}}' '{"description":"campaign-reference-42"}' '{}' 167 do 168 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 169 -X PATCH -H "Authorization: Bearer $BEARER_TOKEN" \ 170 -d "$PATCH_BODY" -w "%{http_code}" -s -o "$LAST_RESPONSE") 171 if [ "$STATUS" != "204" ] 172 then 173 exit_fail "Expected 204 for fountain PATCH, got $STATUS" 174 fi 175 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 176 -H "Authorization: Bearer $BEARER_TOKEN" \ 177 -w "%{http_code}" -s -o "$LAST_RESPONSE") 178 if [ "$STATUS" != "200" ] || ! jq -e '.poll_freq.d_us == 60000000' "$LAST_RESPONSE" > /dev/null 179 then 180 exit_fail "PATCH did not set or preserve poll_freq" 181 fi 182 done 183 # Restore the original polling frequency for the remaining CRUD checks. 184 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 185 -X PATCH -H "Authorization: Bearer $BEARER_TOKEN" \ 186 -d "{\"poll_freq\": $POLL_FREQ}" -w "%{http_code}" -s -o "$LAST_RESPONSE") 187 if [ "$STATUS" != "204" ] 188 then 189 exit_fail "Expected 204 restoring poll_freq, got $STATUS" 190 fi 191 echo "Ok" >&2 192 193 echo -n "Creating fountain with unknown token family fails..." >&2 194 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 195 -X POST \ 196 -H "Authorization: Bearer $BEARER_TOKEN" \ 197 -d "{\"description\": \"broken\", \"poll_freq\": $POLL_FREQ, \"grants\": [{\"token_family_slug\": \"no-such-family\", \"tokens_per_period_limit\": 5, \"tokens_per_period_stash\": 2, \"key_window_size\": 2}]}" \ 198 -w "%{http_code}" \ 199 -s \ 200 -o "$LAST_RESPONSE") 201 if [ "$STATUS" != "404" ] 202 then 203 cat "$LAST_RESPONSE" >&2 204 exit_fail "Expected '404' response. Got instead $STATUS" 205 fi 206 echo "Ok" >&2 207 208 echo -n "Creating fountain with stash above limit fails..." >&2 209 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 210 -X POST \ 211 -H "Authorization: Bearer $BEARER_TOKEN" \ 212 -d "{\"description\": \"broken\", \"poll_freq\": $POLL_FREQ, \"grants\": [{\"token_family_slug\": \"test-discount\", \"tokens_per_period_limit\": 2, \"tokens_per_period_stash\": 5, \"key_window_size\": 2}]}" \ 213 -w "%{http_code}" \ 214 -s \ 215 -o "$LAST_RESPONSE") 216 if [ "$STATUS" != "400" ] 217 then 218 cat "$LAST_RESPONSE" >&2 219 exit_fail "Expected '400' response. Got instead $STATUS" 220 fi 221 echo "Ok" >&2 222 223 # Exercise the default page, hard cap, both directions and invalid cursors. 224 echo -n "Checking bounded fountain pagination..." >&2 225 PAGINATION_IDS=() 226 for ((i=0; i<104; i++)) 227 do 228 STATUS=$(curl "$MERCHANT_URL/private/fountains" -X POST \ 229 -H "Authorization: Bearer $BEARER_TOKEN" \ 230 -d "{\"description\":\"pagination-$i\",\"poll_freq\":$POLL_FREQ,\"grants\":[]}" \ 231 -w "%{http_code}" -s -o "$LAST_RESPONSE") 232 if [ "$STATUS" != "200" ]; then 233 exit_fail "Failed to create pagination fixture ($STATUS)" 234 fi 235 PAGINATION_IDS+=("$(jq -er .fountain_id "$LAST_RESPONSE")") 236 done 237 for QUERY in '' '?limit=1000' '?limit=-9223372036854775808' '?limit=0' 238 do 239 STATUS=$(curl "$MERCHANT_URL/private/fountains$QUERY" \ 240 -H "Authorization: Bearer $BEARER_TOKEN" \ 241 -w "%{http_code}" -s -o "$LAST_RESPONSE") 242 EXPECTED=100 243 [ -z "$QUERY" ] && EXPECTED=20 244 [ "$QUERY" = '?limit=0' ] && EXPECTED=0 245 if [ "$STATUS" != "200" ] || ! jq -e --argjson n "$EXPECTED" \ 246 '.fountains | length == $n' "$LAST_RESPONSE" > /dev/null; then 247 exit_fail "Unexpected page size for $QUERY ($STATUS)" 248 fi 249 done 250 for LIMIT in 17 -17 251 do 252 OFFSET=0 253 [ "$LIMIT" -lt 0 ] && OFFSET=9223372036854775807 254 SEEN='[]' 255 while true 256 do 257 STATUS=$(curl "$MERCHANT_URL/private/fountains?limit=$LIMIT&offset=$OFFSET" \ 258 -H "Authorization: Bearer $BEARER_TOKEN" \ 259 -w "%{http_code}" -s -o "$LAST_RESPONSE") 260 if [ "$STATUS" != "200" ]; then 261 exit_fail "Pagination failed ($STATUS)" 262 fi 263 PAGE=$(jq -c '[.fountains[].fountain_serial]' "$LAST_RESPONSE") 264 [ "$PAGE" = '[]' ] && break 265 if ! jq -ne --argjson p "$PAGE" --argjson o "$OFFSET" --argjson l "$LIMIT" \ 266 'if $l > 0 then ($p == ($p | sort)) and all($p[]; . > $o) 267 else ($p == ($p | sort | reverse)) and all($p[]; . < $o) end' > /dev/null; then 268 exit_fail "Fountain page is unordered or includes its cursor" 269 fi 270 SEEN=$(jq -nc --argjson a "$SEEN" --argjson b "$PAGE" '$a + $b') 271 OFFSET=$(jq -r '.[-1]' <<< "$PAGE") 272 done 273 if ! jq -ne --argjson a "$SEEN" \ 274 '($a | length) == 105 and ($a | unique | length) == 105' > /dev/null; then 275 exit_fail "Pagination skipped or repeated fountains" 276 fi 277 done 278 for QUERY in '?limit=invalid' '?offset=invalid' '?offset=-1' '?offset=9223372036854775808' 279 do 280 STATUS=$(curl "$MERCHANT_URL/private/fountains$QUERY" \ 281 -H "Authorization: Bearer $BEARER_TOKEN" \ 282 -w "%{http_code}" -s -o "$LAST_RESPONSE") 283 [ "$STATUS" = "400" ] || exit_fail "Expected 400 for $QUERY, got $STATUS" 284 done 285 for ID in "${PAGINATION_IDS[@]}" 286 do 287 STATUS=$(curl "$MERCHANT_URL/private/fountains/$ID" -X DELETE \ 288 -H "Authorization: Bearer $BEARER_TOKEN" \ 289 -w "%{http_code}" -s -o "$LAST_RESPONSE") 290 [ "$STATUS" = "204" ] || exit_fail "Failed to remove pagination fixture" 291 done 292 echo "Ok" >&2 293 294 # 295 # LIST AND INSPECT 296 # 297 echo -n "Listing fountains..." >&2 298 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 299 -H "Authorization: Bearer $BEARER_TOKEN" \ 300 -w "%{http_code}" \ 301 -s \ 302 -o "$LAST_RESPONSE") 303 if [ "$STATUS" != "200" ] 304 then 305 cat "$LAST_RESPONSE" >&2 306 exit_fail "Expected '200 OK' response. Got instead $STATUS" 307 fi 308 COUNT=$(jq -e '.fountains | length' < "$LAST_RESPONSE") 309 if [ "$COUNT" != "1" ] 310 then 311 exit_fail "Expected 1 fountain in list, got $COUNT" 312 fi 313 echo "Ok" >&2 314 315 echo -n "Inspecting fountain..." >&2 316 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 317 -H "Authorization: Bearer $BEARER_TOKEN" \ 318 -w "%{http_code}" \ 319 -s \ 320 -o "$LAST_RESPONSE") 321 if [ "$STATUS" != "200" ] 322 then 323 cat "$LAST_RESPONSE" >&2 324 exit_fail "Expected '200 OK' response. Got instead $STATUS" 325 fi 326 DESC=$(jq -e -r .description < "$LAST_RESPONSE") 327 if [ "$DESC" != "campaign-reference-42" ] 328 then 329 exit_fail "Expected description 'campaign-reference-42', got $DESC" 330 fi 331 GRANTS=$(jq -e '.grants | length' < "$LAST_RESPONSE") 332 if [ "$GRANTS" != "2" ] 333 then 334 exit_fail "Expected 2 grants, got $GRANTS" 335 fi 336 if jq -e '.fountain_secret' < "$LAST_RESPONSE" > /dev/null 2>&1 337 then 338 exit_fail "Fountain secret must never be returned by the private API" 339 fi 340 echo "Ok" >&2 341 342 echo -n "Inspecting unknown fountain fails..." >&2 343 STATUS=$(curl "$MERCHANT_URL/private/fountains/does-not-exist" \ 344 -H "Authorization: Bearer $BEARER_TOKEN" \ 345 -w "%{http_code}" \ 346 -s \ 347 -o "$LAST_RESPONSE") 348 if [ "$STATUS" != "404" ] 349 then 350 cat "$LAST_RESPONSE" >&2 351 exit_fail "Expected '404' response. Got instead $STATUS" 352 fi 353 echo "Ok" >&2 354 355 # 356 # WALLET INFO ENDPOINT 357 # 358 echo -n "Fetching fountain info with bearer secret..." >&2 359 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 360 -H "Authorization: Bearer $FOUNTAIN_SECRET" \ 361 -w "%{http_code}" \ 362 -s \ 363 -o "$LAST_RESPONSE") 364 if [ "$STATUS" != "200" ] 365 then 366 cat "$LAST_RESPONSE" >&2 367 exit_fail "Expected '200 OK' response. Got instead $STATUS" 368 fi 369 GRANTS=$(jq -e '.grants | length' < "$LAST_RESPONSE") 370 if [ "$GRANTS" != "2" ] 371 then 372 exit_fail "Expected 2 grants in info, got $GRANTS" 373 fi 374 # At least one issue key must be offered, and at most one per slot of 375 # the grant's key window (slots whose validity period is covered by an 376 # already-listed key share that key, so fewer is legitimate). 377 DISCOUNT_KEYS=$(jq -e '.grants[] | select(.token_family_slug == "test-discount") | .token_family.keys | length' < "$LAST_RESPONSE") 378 if [ "$DISCOUNT_KEYS" -lt 1 ] || [ "$DISCOUNT_KEYS" -gt 3 ] 379 then 380 cat "$LAST_RESPONSE" >&2 381 exit_fail "Expected 1..3 issue keys for test-discount, got $DISCOUNT_KEYS" 382 fi 383 KEYS=$(jq -e '.grants[] | select(.token_family_slug == "test-subscription") | .token_family.keys | length' < "$LAST_RESPONSE") 384 if [ "$KEYS" -lt 1 ] || [ "$KEYS" -gt 2 ] 385 then 386 cat "$LAST_RESPONSE" >&2 387 exit_fail "Expected 1..2 issue keys for test-subscription, got $KEYS" 388 fi 389 # Issue keys must be distinct. 390 UNIQUE_KEYS=$(jq -e '[.grants[] | select(.token_family_slug == "test-discount") | .token_family.keys[]] | unique | length' < "$LAST_RESPONSE") 391 if [ "$UNIQUE_KEYS" != "$DISCOUNT_KEYS" ] 392 then 393 cat "$LAST_RESPONSE" >&2 394 exit_fail "Duplicate issue keys advertised: $DISCOUNT_KEYS listed, $UNIQUE_KEYS distinct" 395 fi 396 echo "Ok" >&2 397 398 echo -n "Fetching fountain info again is idempotent..." >&2 399 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 400 -H "Authorization: Bearer $FOUNTAIN_SECRET" \ 401 -w "%{http_code}" \ 402 -s \ 403 -o "$LAST_RESPONSE") 404 if [ "$STATUS" != "200" ] 405 then 406 cat "$LAST_RESPONSE" >&2 407 exit_fail "Expected '200 OK' response. Got instead $STATUS" 408 fi 409 # Re-polling must be idempotent: no additional keys may be minted. 410 KEYS=$(jq -e '.grants[] | select(.token_family_slug == "test-discount") | .token_family.keys | length' < "$LAST_RESPONSE") 411 if [ "$KEYS" != "$DISCOUNT_KEYS" ] 412 then 413 cat "$LAST_RESPONSE" >&2 414 exit_fail "Re-poll changed issue key count: was $DISCOUNT_KEYS, now $KEYS" 415 fi 416 echo "Ok" >&2 417 418 echo -n "Fetching fountain info with bad secret fails..." >&2 419 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 420 -H "Authorization: Bearer 0000000000000000000000000000000000000000000000000000" \ 421 -w "%{http_code}" \ 422 -s \ 423 -o "$LAST_RESPONSE") 424 if [ "$STATUS" != "401" ] 425 then 426 cat "$LAST_RESPONSE" >&2 427 exit_fail "Expected '401' response. Got instead $STATUS" 428 fi 429 echo "Ok" >&2 430 431 echo -n "Fetching fountain info without secret fails..." >&2 432 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 433 -w "%{http_code}" \ 434 -s \ 435 -o "$LAST_RESPONSE") 436 if [ "$STATUS" != "401" ] 437 then 438 cat "$LAST_RESPONSE" >&2 439 exit_fail "Expected '401' response. Got instead $STATUS" 440 fi 441 echo "Ok" >&2 442 443 # 444 # WITHDRAW VALIDATION ERRORS 445 # 446 echo -n "Withdrawing with bad secret fails..." >&2 447 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 448 -X POST \ 449 -d "{\"fountain_secret\": \"0000000000000000000000000000000000000000000000000000\", \"grants\": []}" \ 450 -w "%{http_code}" \ 451 -s \ 452 -o "$LAST_RESPONSE") 453 if [ "$STATUS" != "401" ] 454 then 455 cat "$LAST_RESPONSE" >&2 456 exit_fail "Expected '401' response. Got instead $STATUS" 457 fi 458 echo "Ok" >&2 459 460 echo -n "Withdrawing for ungranted family fails..." >&2 461 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 462 -X POST \ 463 -d "{\"fountain_secret\": \"$FOUNTAIN_SECRET\", \"grants\": [{\"token_family_slug\": \"no-such-family\", \"envelopes\": []}]}" \ 464 -w "%{http_code}" \ 465 -s \ 466 -o "$LAST_RESPONSE") 467 if [ "$STATUS" != "409" ] 468 then 469 cat "$LAST_RESPONSE" >&2 470 exit_fail "Expected '409' response. Got instead $STATUS" 471 fi 472 echo "Ok" >&2 473 474 echo -n "Withdrawing outside the key window fails..." >&2 475 FUTURE="{\"t_s\": $(date +%s -d "+200 days")}" 476 # A real envelope is required: an empty "envelopes" array is rejected as a 477 # malformed request before the key window is ever resolved. 478 ENVELOPE="{\"cipher\": \"RSA\", \"rsa_blinded_planchet\": \"04\"}" 479 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 480 -X POST \ 481 -d "{\"fountain_secret\": \"$FOUNTAIN_SECRET\", \"grants\": [{\"token_family_slug\": \"test-discount\", \"valid_at\": $FUTURE, \"envelopes\": [$ENVELOPE]}]}" \ 482 -w "%{http_code}" \ 483 -s \ 484 -o "$LAST_RESPONSE") 485 # Check the error code as well: an unknown grant also answers 409, and would 486 # otherwise hide a request that never reached the key window check. 487 if [ "$STATUS" != "409" ] || ! jq -e '.code == 2951' "$LAST_RESPONSE" > /dev/null 488 then 489 cat "$LAST_RESPONSE" >&2 490 exit_fail "Expected '409' TOKEN_KEY_SLOT_OUTSIDE_WINDOW. Got instead $STATUS" 491 fi 492 echo "Ok" >&2 493 494 echo -n "Withdrawing without envelopes fails..." >&2 495 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 496 -X POST \ 497 -d "{\"fountain_secret\": \"$FOUNTAIN_SECRET\", \"grants\": [{\"token_family_slug\": \"test-discount\", \"envelopes\": []}]}" \ 498 -w "%{http_code}" \ 499 -s \ 500 -o "$LAST_RESPONSE") 501 if [ "$STATUS" != "400" ] 502 then 503 cat "$LAST_RESPONSE" >&2 504 exit_fail "Expected '400' response. Got instead $STATUS" 505 fi 506 echo "Ok" >&2 507 508 # 509 # PATCH 510 # 511 echo -n "Wrong envelope cipher is a bad request and does not consume quota..." >&2 512 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 513 -X POST -H "Authorization: Bearer $BEARER_TOKEN" \ 514 -d '{"description":"cipher-validation","poll_freq":{"d_us":60000000},"grants":[{"token_family_slug":"test-discount","tokens_per_period_limit":1,"tokens_per_period_stash":1,"key_window_size":0}]}' \ 515 -w "%{http_code}" -s -o "$LAST_RESPONSE") 516 if [ "$STATUS" != "200" ] 517 then 518 exit_fail "Expected 200 creating cipher-validation fountain, got $STATUS" 519 fi 520 CIPHER_FOUNTAIN_ID=$(jq -er .fountain_id "$LAST_RESPONSE") 521 CIPHER_FOUNTAIN_SECRET=$(jq -er .fountain_secret "$LAST_RESPONSE") 522 # The family uses RSA. These zero-valued CS fields are structurally valid, 523 # so rejection must come from matching the envelope against the issue key. 524 CS_ZERO=0000000000000000000000000000000000000000000000000000 525 CS_ENVELOPE=$(jq -nc --arg z "$CS_ZERO" \ 526 '{cipher:"CS",cs_nonce:$z,cs_blinded_c0:$z,cs_blinded_c1:$z}') 527 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST \ 528 -d "{\"fountain_secret\":\"$CIPHER_FOUNTAIN_SECRET\",\"grants\":[{\"token_family_slug\":\"test-discount\",\"envelopes\":[$CS_ENVELOPE]}]}" \ 529 -w "%{http_code}" -s -o "$LAST_RESPONSE") 530 if [ "$STATUS" != "400" ] || ! jq -e '.code == 26' "$LAST_RESPONSE" > /dev/null 531 then 532 cat "$LAST_RESPONSE" >&2 533 exit_fail "Expected 400 GENERIC_PARAMETER_MALFORMED for wrong cipher, got $STATUS" 534 fi 535 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST \ 536 -d "{\"fountain_secret\":\"$CIPHER_FOUNTAIN_SECRET\",\"grants\":[{\"token_family_slug\":\"test-discount\",\"envelopes\":[$ENVELOPE]}]}" \ 537 -w "%{http_code}" -s -o "$LAST_RESPONSE") 538 if [ "$STATUS" != "200" ] 539 then 540 cat "$LAST_RESPONSE" >&2 541 exit_fail "Wrong-cipher request consumed quota: valid withdrawal returned $STATUS" 542 fi 543 STATUS=$(curl "$MERCHANT_URL/private/fountains/$CIPHER_FOUNTAIN_ID" \ 544 -X DELETE -H "Authorization: Bearer $BEARER_TOKEN" \ 545 -w "%{http_code}" -s -o "$LAST_RESPONSE") 546 if [ "$STATUS" != "204" ] 547 then 548 exit_fail "Expected 204 deleting cipher-validation fountain, got $STATUS" 549 fi 550 echo "Ok" >&2 551 552 echo -n "Fountain quotas are restricted to nonnegative INT8 values..." >&2 553 # Keep the large integer literals as text: passing them through jq arithmetic 554 # can round INT64_MAX and accidentally test a different input. 555 for QUOTAS in \ 556 '"tokens_per_period_limit":0,"tokens_per_period_stash":0' \ 557 '"tokens_per_period_limit":9223372036854775807,"tokens_per_period_stash":9223372036854775807' 558 do 559 QUOTA_GRANT="{\"token_family_slug\":\"test-discount\",$QUOTAS,\"key_window_size\":0}" 560 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 561 -X POST -H "Authorization: Bearer $BEARER_TOKEN" \ 562 -d "{\"description\":\"quota-bounds\",\"poll_freq\":{\"d_us\":60000000},\"grants\":[$QUOTA_GRANT]}" \ 563 -w "%{http_code}" -s -o "$LAST_RESPONSE") 564 if [ "$STATUS" != "200" ] 565 then 566 cat "$LAST_RESPONSE" >&2 567 exit_fail "Expected 200 for valid quota boundary, got $STATUS" 568 fi 569 QUOTA_ID=$(jq -er .fountain_id "$LAST_RESPONSE") 570 STATUS=$(curl "$MERCHANT_URL/private/fountains/$QUOTA_ID" \ 571 -X PATCH -H "Authorization: Bearer $BEARER_TOKEN" \ 572 -d "{\"grants\":[$QUOTA_GRANT]}" \ 573 -w "%{http_code}" -s -o "$LAST_RESPONSE") 574 if [ "$STATUS" != "204" ] 575 then 576 exit_fail "Expected 204 patching valid quota boundary, got $STATUS" 577 fi 578 for BAD_QUOTAS in \ 579 '"tokens_per_period_limit":-1,"tokens_per_period_stash":0' \ 580 '"tokens_per_period_limit":0,"tokens_per_period_stash":-1' \ 581 '"tokens_per_period_limit":-1,"tokens_per_period_stash":-1' \ 582 '"tokens_per_period_limit":-9223372036854775808,"tokens_per_period_stash":0' \ 583 '"tokens_per_period_limit":9223372036854775808,"tokens_per_period_stash":0' 584 do 585 BAD_GRANT="{\"token_family_slug\":\"test-discount\",$BAD_QUOTAS,\"key_window_size\":0}" 586 for METHOD in POST PATCH 587 do 588 QUOTA_URL="$MERCHANT_URL/private/fountains" 589 if [ "$METHOD" = PATCH ] 590 then 591 QUOTA_URL="$QUOTA_URL/$QUOTA_ID" 592 fi 593 STATUS=$(curl "$QUOTA_URL" -X "$METHOD" \ 594 -H "Authorization: Bearer $BEARER_TOKEN" \ 595 -d "{\"description\":\"quota-bounds\",\"poll_freq\":{\"d_us\":60000000},\"grants\":[$BAD_GRANT]}" \ 596 -w "%{http_code}" -s -o "$LAST_RESPONSE") 597 if [ "$STATUS" != "400" ] 598 then 599 cat "$LAST_RESPONSE" >&2 600 exit_fail "Expected 400 for $METHOD with $BAD_QUOTAS, got $STATUS" 601 fi 602 done 603 done 604 STATUS=$(curl "$MERCHANT_URL/private/fountains/$QUOTA_ID" \ 605 -X DELETE -H "Authorization: Bearer $BEARER_TOKEN" \ 606 -w "%{http_code}" -s -o "$LAST_RESPONSE") 607 if [ "$STATUS" != "204" ] 608 then 609 exit_fail "Expected 204 deleting quota test fountain, got $STATUS" 610 fi 611 done 612 echo "Ok" >&2 613 614 echo -n "Offset families advertise covering keys and allow withdrawal..." >&2 615 OFFSET_NOW=$(date +%s) 616 OFFSET_START=$((OFFSET_NOW - 172800)) 617 OFFSET_END=$((OFFSET_NOW + 172800)) 618 STATUS=$(curl "$MERCHANT_URL/private/tokenfamilies" \ 619 -X POST -H "Authorization: Bearer $BEARER_TOKEN" \ 620 -d "{\"slug\":\"test-offset\",\"name\":\"Offset family\",\"description\":\"Offset coverage\",\"kind\":\"discount\",\"valid_after\":{\"t_s\":$OFFSET_START},\"valid_before\":{\"t_s\":$OFFSET_END},\"duration\":{\"d_us\":7140000000},\"validity_granularity\":{\"d_us\":3600000000},\"start_offset\":{\"d_us\":3540000000}}" \ 621 -w "%{http_code}" -s -o "$LAST_RESPONSE") 622 if [ "$STATUS" != "204" ] 623 then 624 cat "$LAST_RESPONSE" >&2 625 exit_fail "Expected 204 creating offset family, got $STATUS" 626 fi 627 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 628 -X POST -H "Authorization: Bearer $BEARER_TOKEN" \ 629 -d '{"description":"offset-coverage","poll_freq":{"d_us":60000000},"grants":[{"token_family_slug":"test-offset","tokens_per_period_limit":3,"tokens_per_period_stash":1,"key_window_size":2}]}' \ 630 -w "%{http_code}" -s -o "$LAST_RESPONSE") 631 if [ "$STATUS" != "200" ] 632 then 633 exit_fail "Expected 200 creating offset fountain, got $STATUS" 634 fi 635 OFFSET_SECRET=$(jq -er .fountain_secret "$LAST_RESPONSE") 636 OFFSET_ID=$(jq -er .fountain_id "$LAST_RESPONSE") 637 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 638 -H "Authorization: Bearer $OFFSET_SECRET" \ 639 -w "%{http_code}" -s -o "$LAST_RESPONSE") 640 if [ "$STATUS" != "200" ] || ! jq -e --argjson now "$OFFSET_NOW" \ 641 '.grants[0].token_family.keys as $keys | 642 ($keys | length) == 3 and 643 $keys[0].signature_validity_start.t_s <= $now and 644 $keys[0].signature_validity_end.t_s >= $now and 645 all(range(1;3); . as $i | 646 $keys[$i].signature_validity_start.t_s <= $keys[$i-1].signature_validity_end.t_s + 1 and 647 $keys[$i].signature_validity_end.t_s > $keys[$i-1].signature_validity_end.t_s)' \ 648 "$LAST_RESPONSE" > /dev/null 649 then 650 cat "$LAST_RESPONSE" >&2 651 exit_fail "Offset family did not advertise a covering key window" 652 fi 653 OFFSET_SLOTS=$(jq -r '.grants[0].token_family.keys[].signature_validity_start.t_s' "$LAST_RESPONSE") 654 for SLOT in $OFFSET_SLOTS 655 do 656 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST \ 657 -d "{\"fountain_secret\":\"$OFFSET_SECRET\",\"grants\":[{\"token_family_slug\":\"test-offset\",\"valid_at\":{\"t_s\":$SLOT},\"envelopes\":[$ENVELOPE]}]}" \ 658 -w "%{http_code}" -s -o "$LAST_RESPONSE") 659 if [ "$STATUS" != "200" ] 660 then 661 cat "$LAST_RESPONSE" >&2 662 exit_fail "Expected 200 withdrawing advertised offset key, got $STATUS" 663 fi 664 done 665 STATUS=$(curl "$MERCHANT_URL/private/fountains/$OFFSET_ID" \ 666 -X DELETE -H "Authorization: Bearer $BEARER_TOKEN" \ 667 -w "%{http_code}" -s -o "$LAST_RESPONSE") 668 if [ "$STATUS" != "204" ] 669 then 670 exit_fail "Expected 204 deleting offset fountain, got $STATUS" 671 fi 672 echo "Ok" >&2 673 674 echo -n "Future keys are bounded by durations from now, not the family start..." >&2 675 # One-hour duration and one-minute granularity distinguish which interval 676 # bounds prefetching. Start halfway through the first future duration. 677 FIRST_START=$(date +%s -d "+30 minutes") 678 FAMILY_REQUEST=$(jq -nc --argjson start "$FIRST_START" \ 679 '{slug:"test-future",name:"Future promotion",description:"Starts soon",kind:"discount",valid_after:{t_s:$start},duration:{d_us:3600000000},validity_granularity:{d_us:60000000}}') 680 STATUS=$(curl "$MERCHANT_URL/private/tokenfamilies" \ 681 -X POST -H "Authorization: Bearer $BEARER_TOKEN" -d "$FAMILY_REQUEST" \ 682 -w "%{http_code}" -s -o "$LAST_RESPONSE") 683 if [ "$STATUS" != "204" ] 684 then 685 exit_fail "Expected 204 creating future token family, got $STATUS" 686 fi 687 STATUS=$(curl "$MERCHANT_URL/private/fountains" \ 688 -X POST -H "Authorization: Bearer $BEARER_TOKEN" \ 689 -d '{"description":"future-start","poll_freq":{"d_us":60000000},"grants":[{"token_family_slug":"test-future","tokens_per_period_limit":3,"tokens_per_period_stash":1,"key_window_size":0}]}' \ 690 -w "%{http_code}" -s -o "$LAST_RESPONSE") 691 if [ "$STATUS" != "200" ] 692 then 693 exit_fail "Expected 200 creating future fountain, got $STATUS" 694 fi 695 FUTURE_SECRET=$(jq -er .fountain_secret "$LAST_RESPONSE") 696 FUTURE_ID=$(jq -er .fountain_id "$LAST_RESPONSE") 697 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 698 -H "Authorization: Bearer $FUTURE_SECRET" \ 699 -w "%{http_code}" -s -o "$LAST_RESPONSE") 700 if [ "$STATUS" != "200" ] || ! jq -e '.grants[0].token_family.keys == []' "$LAST_RESPONSE" > /dev/null 701 then 702 exit_fail "Zero-size window advertised a future key" 703 fi 704 for VALID_AT in null "$(date +%s)" "$FIRST_START" 705 do 706 WITHDRAW_REQUEST=$(jq -nc --arg secret "$FUTURE_SECRET" --argjson at "$VALID_AT" \ 707 --argjson envelope "$ENVELOPE" \ 708 '{fountain_secret:$secret,grants:[({token_family_slug:"test-future",envelopes:[$envelope]} + (if $at == null then {} else {valid_at:{t_s:$at}} end))]}') 709 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST -d "$WITHDRAW_REQUEST" \ 710 -w "%{http_code}" -s -o "$LAST_RESPONSE") 711 if [ "$STATUS" != "409" ] || ! jq -e '.code == 2951' "$LAST_RESPONSE" > /dev/null 712 then 713 cat "$LAST_RESPONSE" >&2 714 exit_fail "Expected 409 for a future family with window 0, got $STATUS" 715 fi 716 done 717 for WINDOW in 1 2 718 do 719 PATCH_REQUEST=$(jq -nc --argjson window "$WINDOW" \ 720 '{grants:[{token_family_slug:"test-future",tokens_per_period_limit:3,tokens_per_period_stash:1,key_window_size:$window}]}') 721 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FUTURE_ID" \ 722 -X PATCH -H "Authorization: Bearer $BEARER_TOKEN" -d "$PATCH_REQUEST" \ 723 -w "%{http_code}" -s -o "$LAST_RESPONSE") 724 if [ "$STATUS" != "204" ] 725 then 726 exit_fail "Expected 204 changing future window, got $STATUS" 727 fi 728 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 729 -H "Authorization: Bearer $FUTURE_SECRET" \ 730 -w "%{http_code}" -s -o "$LAST_RESPONSE") 731 HORIZON=$(($(date +%s) + WINDOW * 3600)) 732 if [ "$STATUS" != "200" ] || ! jq -e --argjson count "$WINDOW" --argjson horizon "$HORIZON" \ 733 '(.grants[0].token_family.keys | length) == $count and 734 all(.grants[0].token_family.keys[]; .signature_validity_start.t_s <= $horizon and .signature_validity_end.t_s <= ($horizon + 3600))' \ 735 "$LAST_RESPONSE" > /dev/null 736 then 737 cat "$LAST_RESPONSE" >&2 738 exit_fail "Future window did not respect its duration-based horizon" 739 fi 740 SLOT_START=$(jq -er '.grants[0].token_family.keys[-1].signature_validity_start.t_s' "$LAST_RESPONSE") 741 WITHDRAW_REQUEST=$(jq -nc --arg secret "$FUTURE_SECRET" --argjson at "$SLOT_START" \ 742 --argjson envelope "$ENVELOPE" \ 743 '{fountain_secret:$secret,grants:[{token_family_slug:"test-future",valid_at:{t_s:$at},envelopes:[$envelope]}]}') 744 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST -d "$WITHDRAW_REQUEST" \ 745 -w "%{http_code}" -s -o "$LAST_RESPONSE") 746 if [ "$STATUS" != "200" ] 747 then 748 exit_fail "Expected 200 for advertised future key, got $STATUS" 749 fi 750 # The next key would start outside the horizon, even though this 751 # family began later than now. Neither info nor withdraw may offer it. 752 WITHDRAW_REQUEST=$(jq --argjson at "$((SLOT_START + 3601))" \ 753 '.grants[0].valid_at = {t_s:$at}' <<< "$WITHDRAW_REQUEST") 754 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST -d "$WITHDRAW_REQUEST" \ 755 -w "%{http_code}" -s -o "$LAST_RESPONSE") 756 if [ "$STATUS" != "409" ] || ! jq -e '.code == 2951' "$LAST_RESPONSE" > /dev/null 757 then 758 exit_fail "Expected 409 beyond the duration-based horizon, got $STATUS" 759 fi 760 done 761 # Even with a nonzero window, a family starting four durations ahead 762 # must not shift the horizon to its own first key. 763 FAR_START=$(date +%s -d "+4 hours") 764 FAMILY_REQUEST=$(jq --argjson start "$FAR_START" \ 765 '.slug = "test-far-future" | .valid_after = {t_s:$start}' <<< "$FAMILY_REQUEST") 766 STATUS=$(curl "$MERCHANT_URL/private/tokenfamilies" \ 767 -X POST -H "Authorization: Bearer $BEARER_TOKEN" -d "$FAMILY_REQUEST" \ 768 -w "%{http_code}" -s -o "$LAST_RESPONSE") 769 if [ "$STATUS" != "204" ] 770 then 771 exit_fail "Expected 204 creating far-future family, got $STATUS" 772 fi 773 PATCH_REQUEST=$(jq '.grants[0].token_family_slug = "test-far-future"' <<< "$PATCH_REQUEST") 774 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FUTURE_ID" \ 775 -X PATCH -H "Authorization: Bearer $BEARER_TOKEN" -d "$PATCH_REQUEST" \ 776 -w "%{http_code}" -s -o "$LAST_RESPONSE") 777 if [ "$STATUS" != "204" ] 778 then 779 exit_fail "Expected 204 changing granted family, got $STATUS" 780 fi 781 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 782 -H "Authorization: Bearer $FUTURE_SECRET" \ 783 -w "%{http_code}" -s -o "$LAST_RESPONSE") 784 if [ "$STATUS" != "200" ] || ! jq -e '.grants[0].token_family.keys == []' "$LAST_RESPONSE" > /dev/null 785 then 786 exit_fail "Far-future family shifted the advertised horizon" 787 fi 788 WITHDRAW_REQUEST=$(jq --argjson at "$FAR_START" \ 789 '.grants[0].token_family_slug = "test-far-future" | .grants[0].valid_at = {t_s:$at}' <<< "$WITHDRAW_REQUEST") 790 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" -X POST -d "$WITHDRAW_REQUEST" \ 791 -w "%{http_code}" -s -o "$LAST_RESPONSE") 792 if [ "$STATUS" != "409" ] || ! jq -e '.code == 2951' "$LAST_RESPONSE" > /dev/null 793 then 794 exit_fail "Expected 409 for far-future family, got $STATUS" 795 fi 796 echo "Ok" >&2 797 798 echo -n "Saving a two-family withdrawal for replay..." >&2 799 REPLAY_REQUEST="{\"fountain_secret\": \"$FOUNTAIN_SECRET\", \"grants\": [{\"token_family_slug\": \"test-discount\", \"envelopes\": [$ENVELOPE]}, {\"token_family_slug\": \"test-subscription\", \"envelopes\": [$ENVELOPE]}]}" 800 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 801 -X POST -d "$REPLAY_REQUEST" \ 802 -w "%{http_code}" -s -o "$LAST_RESPONSE") 803 if [ "$STATUS" != "200" ] || ! jq -e '.grants | length == 2' "$LAST_RESPONSE" > /dev/null 804 then 805 cat "$LAST_RESPONSE" >&2 806 exit_fail "Expected a successful two-family withdrawal, got $STATUS" 807 fi 808 REPLAY_RESPONSE=$(jq -cS . "$LAST_RESPONSE") 809 echo "Ok" >&2 810 811 echo -n "Patching fountain (drop subscription grant)..." >&2 812 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 813 -X PATCH \ 814 -H "Authorization: Bearer $BEARER_TOKEN" \ 815 -d "{\"description\": \"campaign-reference-43\", \"grants\": [{\"token_family_slug\": \"test-discount\", \"tokens_per_period_limit\": 7, \"tokens_per_period_stash\": 3, \"key_window_size\": 2}]}" \ 816 -w "%{http_code}" \ 817 -s \ 818 -o "$LAST_RESPONSE") 819 if [ "$STATUS" != "204" ] 820 then 821 cat "$LAST_RESPONSE" >&2 822 exit_fail "Expected '204' response. Got instead $STATUS" 823 fi 824 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 825 -H "Authorization: Bearer $BEARER_TOKEN" \ 826 -w "%{http_code}" \ 827 -s \ 828 -o "$LAST_RESPONSE") 829 if [ "$STATUS" != "200" ] 830 then 831 cat "$LAST_RESPONSE" >&2 832 exit_fail "Expected '200 OK' response. Got instead $STATUS" 833 fi 834 DESC=$(jq -e -r .description < "$LAST_RESPONSE") 835 if [ "$DESC" != "campaign-reference-43" ] 836 then 837 exit_fail "Expected patched description, got $DESC" 838 fi 839 GRANTS=$(jq -e '.grants | length' < "$LAST_RESPONSE") 840 if [ "$GRANTS" != "1" ] 841 then 842 exit_fail "Expected 1 grant after patch, got $GRANTS" 843 fi 844 echo "Ok" >&2 845 846 echo -n "Withdrawing for dropped grant fails..." >&2 847 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 848 -X POST \ 849 -d "{\"fountain_secret\": \"$FOUNTAIN_SECRET\", \"grants\": [{\"token_family_slug\": \"test-subscription\", \"envelopes\": []}]}" \ 850 -w "%{http_code}" \ 851 -s \ 852 -o "$LAST_RESPONSE") 853 if [ "$STATUS" != "409" ] 854 then 855 cat "$LAST_RESPONSE" >&2 856 exit_fail "Expected '409' response. Got instead $STATUS" 857 fi 858 echo "Ok" >&2 859 860 echo -n "Patching unknown fountain fails..." >&2 861 STATUS=$(curl "$MERCHANT_URL/private/fountains/does-not-exist" \ 862 -X PATCH \ 863 -H "Authorization: Bearer $BEARER_TOKEN" \ 864 -d "{\"description\": \"nope\"}" \ 865 -w "%{http_code}" \ 866 -s \ 867 -o "$LAST_RESPONSE") 868 if [ "$STATUS" != "404" ] 869 then 870 cat "$LAST_RESPONSE" >&2 871 exit_fail "Expected '404' response. Got instead $STATUS" 872 fi 873 echo "Ok" >&2 874 875 # 876 # DELETE 877 # 878 echo -n "Replaying the complete response after token family deletion..." >&2 879 STATUS=$(curl "$MERCHANT_URL/private/tokenfamilies/test-subscription" \ 880 -X DELETE -H "Authorization: Bearer $BEARER_TOKEN" \ 881 -w "%{http_code}" -s -o "$LAST_RESPONSE") 882 if [ "$STATUS" != "204" ] 883 then 884 cat "$LAST_RESPONSE" >&2 885 exit_fail "Expected token family deletion to return 204, got $STATUS" 886 fi 887 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 888 -X POST -d "$REPLAY_REQUEST" \ 889 -w "%{http_code}" -s -o "$LAST_RESPONSE") 890 if [ "$STATUS" != "200" ] || [ "$(jq -cS . "$LAST_RESPONSE")" != "$REPLAY_RESPONSE" ] 891 then 892 cat "$LAST_RESPONSE" >&2 893 exit_fail "Family deletion changed the original withdrawal response ($STATUS)" 894 fi 895 echo "Ok" >&2 896 897 echo -n "Deleting fountain..." >&2 898 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 899 -X DELETE \ 900 -H "Authorization: Bearer $BEARER_TOKEN" \ 901 -w "%{http_code}" \ 902 -s \ 903 -o "$LAST_RESPONSE") 904 if [ "$STATUS" != "204" ] 905 then 906 cat "$LAST_RESPONSE" >&2 907 exit_fail "Expected '204' response. Got instead $STATUS" 908 fi 909 echo "Ok" >&2 910 911 echo -n "Fountain info after deletion fails..." >&2 912 STATUS=$(curl "$MERCHANT_URL/fountain/info" \ 913 -H "Authorization: Bearer $FOUNTAIN_SECRET" \ 914 -w "%{http_code}" \ 915 -s \ 916 -o "$LAST_RESPONSE") 917 if [ "$STATUS" != "401" ] 918 then 919 cat "$LAST_RESPONSE" >&2 920 exit_fail "Expected '401' response. Got instead $STATUS" 921 fi 922 echo "Ok" >&2 923 924 echo -n "Withdrawal after deletion fails..." >&2 925 STATUS=$(curl "$MERCHANT_URL/fountain/withdraw" \ 926 -X POST \ 927 -d "{\"fountain_secret\": \"$FOUNTAIN_SECRET\", \"grants\": []}" \ 928 -w "%{http_code}" -s -o "$LAST_RESPONSE") 929 if [ "$STATUS" != "401" ] 930 then 931 exit_fail "Expected 401 withdrawing from deleted fountain, got $STATUS" 932 fi 933 echo "Ok" >&2 934 935 echo -n "Deleting unknown fountain fails..." >&2 936 STATUS=$(curl "$MERCHANT_URL/private/fountains/$FOUNTAIN_ID" \ 937 -X DELETE \ 938 -H "Authorization: Bearer $BEARER_TOKEN" \ 939 -w "%{http_code}" \ 940 -s \ 941 -o "$LAST_RESPONSE") 942 if [ "$STATUS" != "404" ] 943 then 944 cat "$LAST_RESPONSE" >&2 945 exit_fail "Expected '404' response. Got instead $STATUS" 946 fi 947 echo "Ok" >&2 948 949 echo "Test PASSED" >&2 950 exit 0