Bac à sable
API des assistants de test
Jouez le rôle du payeur depuis vos propres tests, sans navigateur : terminer un paiement par carte, payer ou faire expirer un code espèces.
Sur cette page
Les assistants de test permettent à un test automatisé de faire ce qu’une personne fait en mode
test : taper une carte de test sur la page de paiement, ou payer un code espèces chez un agent. Ils
se trouvent sous /v1/test_helpers et n’acceptent qu’une clé secrète de test. Avec une clé de
production, ils répondent 403, permission_error, test_mode_only.
Un assistant agit sur une tentative (ch_…) : le latest_charge d’une intention de paiement
qui attend son payeur, en requires_action. Il joue le rôle du payeur ou du réseau, rien de plus :
le paiement change ensuite comme il le ferait en production, par la notification du prestataire ou
une vérification de statut, en général en quelques secondes. Relisez l’intention de paiement, ou
attendez le webhook.
Terminer un paiement par carte
POST /v1/test_helpers/charges/{charge}/complete soumet un numéro de carte sur la page de paiement
d’une tentative carte en attente, comme le ferait votre client.
card_number: l’une des cartes de test, avec ou sans espaces. Tout autre numéro est refusé.three_d_secure, facultatif : pour la carte 3-D Secure, la réponse au défi simulé (voir la référence de l’API).
curl https://pay-api.adpromax.com/v1/test_helpers/charges/ch_01k7d2j8m3n6b9v2c5x8z1a4qd/complete \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{"card_number": "4242424242424242"}'const charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
const path = `/v1/test_helpers/charges/${charge}/complete`
await fetch(`https://pay-api.adpromax.com${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ card_number: '4242424242424242' }),
})interface CompleteCharge {
/** One of the test cards, with or without spaces. */
card_number: string
}
const charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
const path = `/v1/test_helpers/charges/${charge}/complete`
const body: CompleteCharge = { card_number: '4242424242424242' }
await fetch(`https://pay-api.adpromax.com${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})import os
import requests
charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd"
requests.post(
f"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete",
auth=(os.environ["APX_SECRET_KEY"], ""),
json={"card_number": "4242424242424242"},
)<?php
$charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd';
$path = "/v1/test_helpers/charges/{$charge}/complete";
$ch = curl_init('https://pay-api.adpromax.com' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['card_number' => '4242424242424242']),
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);require 'json'
require 'net/http'
charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
uri = URI("https://pay-api.adpromax.com/v1/test_helpers/charges/#{charge}/complete")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(card_number: '4242424242424242')
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
endpackage main
import (
"log"
"net/http"
"os"
"strings"
)
func main() {
charge := "ch_01k7d2j8m3n6b9v2c5x8z1a4qd"
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/test_helpers/charges/"+charge+"/complete",
strings.NewReader(`{"card_number": "4242424242424242"}`))
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
res.Body.Close()
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Complete {
public static void main(String[] args) throws Exception {
String charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
String body = """
{"card_number": "4242424242424242"}
""";
HttpRequest request = HttpRequest.newBuilder(URI.create(
"https://pay-api.adpromax.com/v1/test_helpers/charges/" + charge + "/complete"))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.discarding());
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
using var response = await http.PostAsJsonAsync(
$"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete",
new { card_number = "4242424242424242" });// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
reqwest::blocking::Client::new()
.post(format!(
"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete"
))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.json(&json!({ "card_number": "4242424242424242" }))
.send()?;
Ok(())
}Payer, faire expirer, refuser ou suspendre un code espèces
Quatre assistants agissent sur une tentative espèces en attente, comme le ferait le réseau de
paiement. Chacun est un POST sans corps, sauf pay :
| Assistant | Ce qui se passe |
|---|---|
/v1/test_helpers/cash_vouchers/{charge}/pay | Le code est payé. amount, facultatif, en centimes : payer un autre montant que celui du bon. |
/v1/test_helpers/cash_vouchers/{charge}/expire | Le code atteint son échéance sans être payé. |
/v1/test_helpers/cash_vouchers/{charge}/refuse | Le réseau refuse le code. |
/v1/test_helpers/cash_vouchers/{charge}/suspend | Le code est mis en attente ; payez-le ensuite avec pay. |
L’effet de chaque issue sur le paiement est décrit dans Scénarios de test en espèces.
CH=ch_01k7d5f3a1s5d9f2g6h4j8k3nb
curl -X POST https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
-u "$APX_SECRET_KEY:"
# or pay another amount: half of a 150.00 MAD payment
curl https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{"amount": 7500}'const charge = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
const helper = action =>
`https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/${charge}/${action}`
const headers = {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
}
await fetch(helper('pay'), { method: 'POST', headers })
// or: helper('expire'), helper('refuse'), helper('suspend'),
// or pay another amount:
// fetch(helper('pay'), { method: 'POST', headers,
// body: JSON.stringify({ amount: 7500 }) })type CashAction = 'pay' | 'expire' | 'refuse' | 'suspend'
const charge = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
const helper = (action: CashAction): string =>
`https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/${charge}/${action}`
const headers = {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
}
await fetch(helper('pay'), { method: 'POST', headers })
// or: helper('expire'), helper('refuse'), helper('suspend'),
// or pay another amount:
// fetch(helper('pay'), { method: 'POST', headers,
// body: JSON.stringify({ amount: 7500 }) })import os
import requests
AUTH = (os.environ["APX_SECRET_KEY"], "")
charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb"
def cash_helper(action, body=None):
url = f"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{charge}/{action}"
return requests.post(url, auth=AUTH, json=body or {})
cash_helper("pay")
# or "expire", "refuse", "suspend"; another amount:
# cash_helper("pay", {"amount": 7500})<?php
function cash_helper(string $charge, string $action, array $body = []): void
{
$path = "/v1/test_helpers/cash_vouchers/{$charge}/{$action}";
$ch = curl_init('https://pay-api.adpromax.com' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode((object) $body),
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
}
cash_helper('ch_01k7d5f3a1s5d9f2g6h4j8k3nb', 'pay');
// or 'expire', 'refuse', 'suspend'; another amount:
// cash_helper('ch_…', 'pay', ['amount' => 7500]);require 'json'
require 'net/http'
CHARGE = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
def cash_helper(action, body = {})
uri = URI("https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/#{CHARGE}/#{action}")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(body)
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
end
cash_helper('pay')
# or 'expire', 'refuse', 'suspend'; another amount:
# cash_helper('pay', amount: 7500)package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
)
const charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb"
func cashHelper(action string, body map[string]any) error {
payload, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/"+charge+"/"+action,
bytes.NewReader(payload))
if err != nil {
return err
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
return res.Body.Close()
}
func main() {
if err := cashHelper("pay", map[string]any{}); err != nil {
log.Fatal(err)
}
// or "expire", "refuse", "suspend"; another amount:
// cashHelper("pay", map[string]any{"amount": 7500})
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Cash {
static final String CHARGE = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";
static void cashHelper(String action, String body) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(
"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/"
+ CHARGE + "/" + action))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.discarding());
}
public static void main(String[] args) throws Exception {
cashHelper("pay", "{}");
// or "expire", "refuse", "suspend"; another amount:
// cashHelper("pay", "{\"amount\": 7500}");
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
const string charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";
Task<HttpResponseMessage> CashHelper(string action, object? body = null) =>
http.PostAsJsonAsync(
$"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{charge}/{action}",
body ?? new { });
await CashHelper("pay");
// or "expire", "refuse", "suspend"; another amount:
// await CashHelper("pay", new { amount = 7500 });// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
use serde_json::{json, Value};
const CHARGE: &str = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";
fn cash_helper(action: &str, body: Value) -> Result<(), Box<dyn std::error::Error>> {
reqwest::blocking::Client::new()
.post(format!(
"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{CHARGE}/{action}"
))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.json(&body)
.send()?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
cash_helper("pay", json!({}))?;
// or "expire", "refuse", "suspend"; another amount:
// cash_helper("pay", json!({ "amount": 7500 }))?;
Ok(())
}Un paiement par carte complet, sans navigateur
Un test de votre intégration peut faire un paiement de bout en bout : créer une intention de
paiement, la confirmer pour une carte, terminer sa tentative avec une carte de test, puis attendre
succeeded. Chaque création et chaque confirmation porte sa propre Idempotency-Key, nouvelle à
chaque exécution du test.
API=https://pay-api.adpromax.com/v1
RUN=$(date +%s)
PI=$(curl -s $API/payment_intents -u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-$RUN-create" \
-d '{"amount": 50000, "currency": "mad",
"payment_method_types": ["card"]}' | jq -r .id)
curl -s $API/payment_intents/$PI/confirm -u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-$RUN-confirm" \
-d '{"payment_method_type": "card",
"return_url": "https://shop.example.com/done"}' > /dev/null
CH=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:" \
| jq -r .latest_charge)
curl -s $API/test_helpers/charges/$CH/complete -u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{"card_number": "4242424242424242"}' > /dev/null
for i in 1 2 3 4 5 6 7 8 9 10; do
STATUS=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:" \
| jq -r .status)
[ "$STATUS" = succeeded ] && break
sleep 1
done
echo "$PI $STATUS"import { randomUUID } from 'node:crypto'
const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
const run = randomUUID()
async function call(method, path, body, key) {
const headers = { Authorization: auth }
if (body !== undefined)
headers['Content-Type'] = 'application/json'
if (key !== undefined)
headers['Idempotency-Key'] = `${run}-${key}`
const response = await fetch(`${api}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
const answer = await response.json()
if (!response.ok)
throw new Error(answer.error.message)
return answer
}
const intent = await call('POST', '/payment_intents', {
amount: 50000,
currency: 'mad',
payment_method_types: ['card'],
}, 'create')
await call('POST', `/payment_intents/${intent.id}/confirm`, {
payment_method_type: 'card',
return_url: 'https://shop.example.com/done',
}, 'confirm')
const charge = (await call('GET', `/payment_intents/${intent.id}`)).latest_charge
await call('POST', `/test_helpers/charges/${charge}/complete`, {
card_number: '4242424242424242',
})
let status
for (let i = 0; i < 10 && status !== 'succeeded'; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
status = (await call('GET', `/payment_intents/${intent.id}`)).status
}
console.log(intent.id, status)import { randomUUID } from 'node:crypto'
interface PaymentIntent {
id: string
status: string
latest_charge: string | null
}
interface ApiError {
error: { type: string, code: string, message: string, request_id: string }
}
const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
const run = randomUUID()
async function call<T = PaymentIntent>(
method: 'GET' | 'POST',
path: string,
body?: object,
key?: string,
): Promise<T> {
const headers: Record<string, string> = { Authorization: auth }
if (body !== undefined)
headers['Content-Type'] = 'application/json'
if (key !== undefined)
headers['Idempotency-Key'] = `${run}-${key}`
const response = await fetch(`${api}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
if (!response.ok) {
const { error } = (await response.json()) as ApiError
throw new Error(`${error.code}: ${error.message}`)
}
return (await response.json()) as T
}
const intent = await call('POST', '/payment_intents', {
amount: 50000,
currency: 'mad',
payment_method_types: ['card'],
}, 'create')
await call('POST', `/payment_intents/${intent.id}/confirm`, {
payment_method_type: 'card',
return_url: 'https://shop.example.com/done',
}, 'confirm')
const charge = (await call('GET', `/payment_intents/${intent.id}`)).latest_charge
await call<unknown>('POST', `/test_helpers/charges/${charge}/complete`, {
card_number: '4242424242424242',
})
let status: string | undefined
for (let i = 0; i < 10 && status !== 'succeeded'; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
status = (await call('GET', `/payment_intents/${intent.id}`)).status
}
console.log(intent.id, status)import os
import time
import uuid
import requests
API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
RUN = uuid.uuid4().hex
def call(method, path, body=None, key=None):
headers = {"Idempotency-Key": f"{RUN}-{key}"} if key else {}
response = requests.request(
method, f"{API}{path}", auth=AUTH, json=body, headers=headers
)
answer = response.json()
response.raise_for_status()
return answer
intent = call("POST", "/payment_intents", {
"amount": 50000,
"currency": "mad",
"payment_method_types": ["card"],
}, "create")
call("POST", f"/payment_intents/{intent['id']}/confirm", {
"payment_method_type": "card",
"return_url": "https://shop.example.com/done",
}, "confirm")
charge = call("GET", f"/payment_intents/{intent['id']}")["latest_charge"]
call("POST", f"/test_helpers/charges/{charge}/complete", {
"card_number": "4242424242424242",
})
status = None
for _ in range(10):
time.sleep(1)
status = call("GET", f"/payment_intents/{intent['id']}")["status"]
if status == "succeeded":
break
print(intent["id"], status)<?php
function call(
string $method,
string $path,
?array $body = null,
?string $key = null
): array {
$headers = ['Content-Type: application/json'];
if ($key !== null) {
$headers[] = "Idempotency-Key: {$key}";
}
$ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$answer = json_decode(curl_exec($ch), true);
return $answer;
}
$run = bin2hex(random_bytes(8));
$intent = call('POST', '/payment_intents', [
'amount' => 50000,
'currency' => 'mad',
'payment_method_types' => ['card'],
], "{$run}-create");
call('POST', "/payment_intents/{$intent['id']}/confirm", [
'payment_method_type' => 'card',
'return_url' => 'https://shop.example.com/done',
], "{$run}-confirm");
$charge = call('GET', "/payment_intents/{$intent['id']}")['latest_charge'];
call('POST', "/test_helpers/charges/{$charge}/complete", [
'card_number' => '4242424242424242',
]);
for ($i = 0, $status = null; $i < 10 && $status !== 'succeeded'; $i++) {
sleep(1);
$status = call('GET', "/payment_intents/{$intent['id']}")['status'];
}
echo $intent['id'], ' ', $status, PHP_EOL;require 'json'
require 'minitest/autorun'
require 'net/http'
require 'securerandom'
class CardPaymentTest < Minitest::Test
API = 'https://pay-api.adpromax.com/v1'
RUN = SecureRandom.uuid
def api(method, path, body = nil, key = nil)
uri = URI("#{API}#{path}")
request = (method == :get ? Net::HTTP::Get : Net::HTTP::Post).new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Idempotency-Key'] = "#{RUN}-#{key}" if key
if body
request['Content-Type'] = 'application/json'
request.body = JSON.generate(body)
end
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
answer = JSON.parse(response.body)
raise answer.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)
answer
end
def test_a_card_payment_succeeds
intent = api(:post, '/payment_intents', {
amount: 50_000,
currency: 'mad',
payment_method_types: ['card']
}, 'create')
api(:post, "/payment_intents/#{intent['id']}/confirm", {
payment_method_type: 'card',
return_url: 'https://shop.example.com/done'
}, 'confirm')
charge = api(:get, "/payment_intents/#{intent['id']}")['latest_charge']
api(:post, "/test_helpers/charges/#{charge}/complete",
{ card_number: '4242424242424242' })
status = nil
10.times do
sleep 1
status = api(:get, "/payment_intents/#{intent['id']}")['status']
break if status == 'succeeded'
end
assert_equal 'succeeded', status
end
endpackage main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"testing"
"time"
)
const api = "https://pay-api.adpromax.com/v1"
type paymentIntent struct {
ID string `json:"id"`
Status string `json:"status"`
LatestCharge string `json:"latest_charge"`
}
func call(t *testing.T, method, path string, body any, key string) paymentIntent {
t.Helper()
var payload io.Reader
if body != nil {
encoded, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
payload = bytes.NewReader(encoded)
}
req, err := http.NewRequest(method, api+path, payload)
if err != nil {
t.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if key != "" {
req.Header.Set("Idempotency-Key", key)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode >= 400 {
t.Fatalf("%s %s: %s", method, path, res.Status)
}
var answer paymentIntent
if err := json.NewDecoder(res.Body).Decode(&answer); err != nil {
t.Fatal(err)
}
return answer
}
func TestCardPayment(t *testing.T) {
run := fmt.Sprint(time.Now().UnixNano())
intent := call(t, "POST", "/payment_intents", map[string]any{
"amount": 50000,
"currency": "mad",
"payment_method_types": []string{"card"},
}, run+"-create")
path := "/payment_intents/" + intent.ID
call(t, "POST", path+"/confirm", map[string]any{
"payment_method_type": "card",
"return_url": "https://shop.example.com/done",
}, run+"-confirm")
charge := call(t, "GET", path, nil, "").LatestCharge
call(t, "POST", "/test_helpers/charges/"+charge+"/complete",
map[string]any{"card_number": "4242424242424242"}, "")
status := ""
for i := 0; i < 10 && status != "succeeded"; i++ {
time.Sleep(time.Second)
status = call(t, "GET", path, nil, "").Status
}
if status != "succeeded" {
t.Fatalf("%s is %q, want succeeded", intent.ID, status)
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CardPaymentTest {
static final String API = "https://pay-api.adpromax.com/v1";
static final String RUN = UUID.randomUUID().toString();
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String body, String key)
throws Exception {
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.method(method, body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
if (key != null) {
request.header("Idempotency-Key", RUN + "-" + key);
}
HttpResponse<String> response = HTTP.send(
request.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException(response.body());
}
return response.body();
}
// No JSON library here: a small regex reads the first field of that name.
static String field(String json, String name) {
Matcher match = Pattern
.compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
.matcher(json);
if (!match.find()) {
throw new IllegalStateException("no " + name + " in " + json);
}
return match.group(1);
}
public static void main(String[] args) throws Exception {
String intent = field(call("POST", "/payment_intents", """
{
"amount": 50000,
"currency": "mad",
"payment_method_types": ["card"]
}
""", "create"), "id");
String path = "/payment_intents/" + intent;
call("POST", path + "/confirm", """
{
"payment_method_type": "card",
"return_url": "https://shop.example.com/done"
}
""", "confirm");
String charge = field(call("GET", path, null, null), "latest_charge");
call("POST", "/test_helpers/charges/" + charge + "/complete", """
{"card_number": "4242424242424242"}
""", null);
String status = null;
for (int i = 0; i < 10 && !"succeeded".equals(status); i++) {
Thread.sleep(1000);
status = field(call("GET", path, null, null), "status");
}
if (!"succeeded".equals(status)) {
throw new AssertionError(intent + " is " + status);
}
System.out.println(intent + " " + status);
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var api = "https://pay-api.adpromax.com/v1";
var run = Guid.NewGuid().ToString();
async Task<PaymentIntent> Call(
HttpMethod method, string path, object? body = null, string? key = null)
{
using var request = new HttpRequestMessage(method, api + path);
if (body is not null)
request.Content = JsonContent.Create(body);
if (key is not null)
request.Headers.Add("Idempotency-Key", $"{run}-{key}");
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
throw new InvalidOperationException(failure?.Error.Message);
}
return (await response.Content.ReadFromJsonAsync<PaymentIntent>(json))!;
}
var intent = await Call(HttpMethod.Post, "/payment_intents", new
{
amount = 50000,
currency = "mad",
payment_method_types = new[] { "card" },
}, "create");
var path = $"/payment_intents/{intent.Id}";
await Call(HttpMethod.Post, $"{path}/confirm", new
{
payment_method_type = "card",
return_url = "https://shop.example.com/done",
}, "confirm");
var charge = (await Call(HttpMethod.Get, path)).LatestCharge;
await Call(HttpMethod.Post, $"/test_helpers/charges/{charge}/complete",
new { card_number = "4242424242424242" });
string? status = null;
for (var i = 0; i < 10 && status != "succeeded"; i++)
{
await Task.Delay(1000);
status = (await Call(HttpMethod.Get, path)).Status;
}
if (status != "succeeded")
throw new InvalidOperationException($"{intent.Id} is {status}");
Console.WriteLine($"{intent.Id} {status}");
record PaymentIntent(string Id, string Status, string? LatestCharge);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
// Run it with `cargo test`.
fn main() {}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::{blocking::Client, Method};
use serde_json::{json, Value};
const API: &str = "https://pay-api.adpromax.com/v1";
type Error = Box<dyn std::error::Error>;
fn call(
method: Method,
path: &str,
body: Option<Value>,
key: Option<String>,
) -> Result<Value, Error> {
let mut request = Client::new()
.request(method, format!("{API}{path}"))
.bearer_auth(std::env::var("APX_SECRET_KEY")?);
if let Some(body) = body {
request = request.json(&body);
}
if let Some(key) = key {
request = request.header("Idempotency-Key", key);
}
let response = request.send()?;
let failed = !response.status().is_success();
let answer: Value = response.json()?;
if failed {
let message = answer["error"]["message"].as_str();
return Err(message.unwrap_or("request failed").into());
}
Ok(answer)
}
#[test]
fn a_card_payment_succeeds() -> Result<(), Error> {
let run = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
let intent = call(
Method::POST,
"/payment_intents",
Some(json!({
"amount": 50000,
"currency": "mad",
"payment_method_types": ["card"]
})),
Some(format!("{run}-create")),
)?;
let id = intent["id"].as_str().ok_or("no id")?;
let path = format!("/payment_intents/{id}");
call(
Method::POST,
&format!("{path}/confirm"),
Some(json!({
"payment_method_type": "card",
"return_url": "https://shop.example.com/done"
})),
Some(format!("{run}-confirm")),
)?;
let waiting = call(Method::GET, &path, None, None)?;
let charge = waiting["latest_charge"].as_str().ok_or("no charge")?;
call(
Method::POST,
&format!("/test_helpers/charges/{charge}/complete"),
Some(json!({ "card_number": "4242424242424242" })),
None,
)?;
let mut status = String::new();
for _ in 0..10 {
thread::sleep(Duration::from_secs(1));
let current = call(Method::GET, &path, None, None)?;
status = current["status"].as_str().unwrap_or_default().to_owned();
if status == "succeeded" {
break;
}
}
assert_eq!(status, "succeeded", "{id}");
Ok(())
}
}Ailleurs en mode test
- La page du bon que voit votre client porte Simuler le paiement et les autres issues espèces.
- Sur le tableau de bord, la page d’un paiement de test a une carte Assistants de test avec les mêmes actions.
- Ni l’une ni l’autre n’existe en production, où seul un vrai paiement fait avancer un paiement.