Files
Qris-Soundbox/scripts/smoke.mjs

523 lines
18 KiB
JavaScript

import { createHmac } from "node:crypto";
const PORT = process.env.PORT || "3100";
const BASE = process.env.BASE_URL || `http://127.0.0.1:${PORT}`;
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "admin-dev-token";
const DEVICE_TOKEN = process.env.DEVICE_TOKEN || "device-dev-token";
const SECRET = process.env.INTEGRATION_WEBHOOK_SECRET || "dev-callback-secret";
function short(data) {
const json = typeof data === 'string' ? data : JSON.stringify(data || {});
return json.length > 180 ? `${json.slice(0, 180)}...` : json;
}
async function req(path, options = {}) {
const response = await fetch(`${BASE}${path}`, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...(options.headers || {})
},
body: Object.prototype.hasOwnProperty.call(options, 'body') ? JSON.stringify(options.body) : undefined
});
const text = await response.text();
let body = null;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = text;
}
if (!response.ok) {
throw new Error(`${options._label || path} failed: ${response.status}`);
}
console.log(`${options._label || `${options.method || 'GET'} ${path}`} => ${response.status} ${short(body)}`);
return body;
}
async function reqExpect(path, expectedStatus, options = {}) {
const response = await fetch(`${BASE}${path}`, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...(options.headers || {})
},
body: Object.prototype.hasOwnProperty.call(options, 'body') ? JSON.stringify(options.body) : undefined
});
const text = await response.text();
let body = null;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = text;
}
if (response.status !== expectedStatus) {
throw new Error(`${options._label || path} expected ${expectedStatus}, got ${response.status}`);
}
console.log(`${options._label || `${options.method || 'GET'} ${path}`} => ${response.status} ${short(body)}`);
return body;
}
async function reqAdmin(path, opts = {}) {
return req(path, { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${ADMIN_TOKEN}` } });
}
async function reqDevice(path, opts = {}) {
return req(path, { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${DEVICE_TOKEN}` } });
}
(async () => {
await req('/health', { _label: 'GET /health' });
await req('/admin/login', { method: 'POST', body: { username: 'admin', password: 'admin' }, _label: 'POST /admin/login' });
await reqAdmin('/admin/seed/status', { _label: 'GET /admin/seed/status' });
const ts = Date.now();
const merchant = await reqAdmin('/admin/merchants', {
method: 'POST',
body: {
legal_name: `Smoke Merchant ${ts}`,
brand_name: `SMK-${ts}`,
settlement_account_reference: `bank:${ts}`,
settlement_account_type: 'merchant_bank_account',
payout_mode: 'merchant_direct'
},
_label: 'POST /admin/merchants'
});
const merchantId = merchant?.data?.id;
const outlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
method: 'POST',
body: { name: `Outlet ${ts}` },
_label: 'POST /admin/merchants/:id/outlets'
});
const outletId = outlet?.data?.id;
const terminal = await reqAdmin(`/admin/outlets/${outletId}/terminals`, {
method: 'POST',
body: { terminal_code: `TERM-${ts}`, qr_mode: 'static' },
_label: 'POST /admin/outlets/:id/terminals'
});
const terminalId = terminal?.data?.id;
const device = await reqAdmin('/admin/devices', {
method: 'POST',
body: {
device_code: `DEV-${ts}`,
vendor: 'acme',
model: 'v1',
communication_mode: 'mqtt',
status: 'active'
},
_label: 'POST /admin/devices'
});
const deviceId = device?.data?.id;
await reqAdmin(`/admin/devices/${deviceId}/bind`, {
method: 'POST',
body: {
merchant_id: merchantId,
outlet_id: outletId,
terminal_id: terminalId
},
_label: 'POST /admin/devices/:id/bind'
});
await reqDevice('/device/heartbeat', {
method: 'POST',
body: {
device_id: deviceId,
timestamp: new Date().toISOString(),
firmware_version: '1.2.3',
network_strength: 88,
battery_level: 77,
state: 'idle'
},
_label: 'POST /device/heartbeat'
});
const tx = await reqAdmin('/admin/transactions', {
method: 'POST',
body: {
partner_reference: `PR-${ts}`,
merchant_id: merchantId,
outlet_id: outletId,
terminal_id: terminalId,
device_id: deviceId,
amount: 19900,
currency: 'IDR',
qr_mode: 'static',
initiation_mode: 'static',
status: 'initiated'
},
_label: 'POST /admin/transactions'
});
const txId = tx?.data?.id;
const callback = {
partner_reference: `PR-${ts}`,
partner_txn_id: `PTX-${ts}`,
amount: 19900,
currency: 'IDR',
payment_status: 'paid',
status: 'paid',
paid_at: new Date().toISOString()
};
const signature = createHmac('sha256', SECRET).update(JSON.stringify(callback)).digest('hex');
await req('/integrations/qris/callback', {
method: 'POST',
headers: { 'X-Partner-Signature': signature },
body: { ...callback, signature },
_label: 'POST /integrations/qris/callback'
});
await req('/integrations/qris/callback', {
method: 'POST',
headers: { 'X-Partner-Signature': signature },
body: { ...callback, signature },
_label: 'POST /integrations/qris/callback duplicate'
});
await reqExpect('/integrations/qris/callback', 401, {
method: 'POST',
headers: { 'X-Partner-Signature': 'bad-signature' },
body: { ...callback, signature: 'bad-signature' },
_label: 'POST /integrations/qris/callback invalid signature'
});
await reqAdmin(`/admin/transactions/${txId}`, { _label: 'GET /admin/transactions/:id' });
await reqAdmin(`/admin/transactions/${txId}/events`, { _label: 'GET /admin/transactions/:id/events' });
await reqAdmin(`/admin/ledger-entries?transaction_id=${txId}`, { _label: 'GET /admin/ledger-entries' });
await reqAdmin(`/admin/audit-logs?entity_id=${txId}`, { _label: 'GET /admin/audit-logs' });
await reqAdmin(`/admin/transactions/${txId}/heartbeats`, { _label: 'GET /admin/transactions/:id/heartbeats' });
await reqAdmin(`/admin/devices/${deviceId}/heartbeats`, { _label: 'GET /admin/devices/:id/heartbeats' });
await reqAdmin(`/admin/devices/${deviceId}`, { _label: 'GET /admin/devices/:id health summary' });
await reqAdmin('/admin/notifications/failed', { _label: 'GET /admin/notifications/failed' });
await reqAdmin(`/admin/transactions/${txId}/retry-notification`, {
method: 'POST',
body: {},
_label: 'POST /admin/transactions/:id/retry-notification'
});
await reqAdmin('/admin/dashboard/summary', { _label: 'GET /admin/dashboard/summary' });
const noBindingOutlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
method: 'POST',
body: { name: `No Binding Outlet ${ts}` },
_label: 'POST /admin/merchants/:id/outlets no-binding'
});
const noBindingOutletId = noBindingOutlet?.data?.id;
const noBindingTerminal = await reqAdmin(`/admin/outlets/${noBindingOutletId}/terminals`, {
method: 'POST',
body: { terminal_code: `TERM-NB-${ts}`, qr_mode: 'static' },
_label: 'POST /admin/outlets/:id/terminals no-binding'
});
const noBindingTerminalId = noBindingTerminal?.data?.id;
const noBindingTx = await reqAdmin('/admin/transactions', {
method: 'POST',
body: {
partner_reference: `PR-NB-${ts}`,
merchant_id: merchantId,
outlet_id: noBindingOutletId,
terminal_id: noBindingTerminalId,
amount: 9900,
currency: 'IDR',
qr_mode: 'static',
initiation_mode: 'static',
status: 'initiated'
},
_label: 'POST /admin/transactions no-binding'
});
const noBindingCallback = {
partner_reference: `PR-NB-${ts}`,
partner_txn_id: `PTX-NB-${ts}`,
amount: 9900,
currency: 'IDR',
payment_status: 'paid',
status: 'paid',
paid_at: new Date().toISOString()
};
const noBindingSignature = createHmac('sha256', SECRET).update(JSON.stringify(noBindingCallback)).digest('hex');
await req('/integrations/qris/callback', {
method: 'POST',
headers: { 'X-Partner-Signature': noBindingSignature },
body: { ...noBindingCallback, signature: noBindingSignature },
_label: 'POST /integrations/qris/callback no-binding'
});
await reqAdmin(`/admin/ledger-entries?transaction_id=${noBindingTx?.data?.id}`, {
_label: 'GET /admin/ledger-entries no-binding'
});
await reqAdmin('/admin/notifications/failed', { _label: 'GET /admin/notifications/failed no-binding' });
const dynamicOutlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
method: 'POST',
body: { name: `Dynamic API Outlet ${ts}` },
_label: 'POST /admin/merchants/:id/outlets dynamic-api'
});
const dynamicOutletId = dynamicOutlet?.data?.id;
const dynamicTerminal = await reqAdmin(`/admin/outlets/${dynamicOutletId}/terminals`, {
method: 'POST',
body: { terminal_code: `TERM-DYN-${ts}`, qr_mode: 'dynamic_api' },
_label: 'POST /admin/outlets/:id/terminals dynamic-api'
});
const dynamicTerminalId = dynamicTerminal?.data?.id;
const dynamicDevice = await reqAdmin('/admin/devices', {
method: 'POST',
body: {
device_code: `DEV-API-${ts}`,
vendor: 'acme',
model: 'api-v1',
communication_mode: 'api',
capability_profile_json: {
dynamic_qr: { api_direct: true, mqtt: false },
flows: ['dynamic_qr:api_direct', 'static_payment_notification']
},
status: 'active'
},
_label: 'POST /admin/devices dynamic-api'
});
const dynamicDeviceId = dynamicDevice?.data?.id;
await reqAdmin(`/admin/devices/${dynamicDeviceId}/bind`, {
method: 'POST',
body: {
merchant_id: merchantId,
outlet_id: dynamicOutletId,
terminal_id: dynamicTerminalId
},
_label: 'POST /admin/devices/:id/bind dynamic-api'
});
await reqExpect('/device/transactions/dynamic-qr', 400, {
method: 'POST',
headers: { Authorization: `Bearer ${DEVICE_TOKEN}` },
body: {
device_id: deviceId,
terminal_id: dynamicTerminalId,
amount: 15000,
currency: 'IDR',
request_id: `DYN-STATIC-${ts}`
},
_label: 'POST /device/transactions/dynamic-qr unsupported device'
});
const dynamicRequestId = `DYN-REQ-${ts}`;
const dynamicQr = await reqDevice('/device/transactions/dynamic-qr', {
method: 'POST',
headers: { 'Idempotency-Key': dynamicRequestId },
body: {
device_id: dynamicDeviceId,
terminal_id: dynamicTerminalId,
amount: 32100,
currency: 'IDR',
request_id: dynamicRequestId
},
_label: 'POST /device/transactions/dynamic-qr'
});
const dynamicQrReplay = await reqDevice('/device/transactions/dynamic-qr', {
method: 'POST',
headers: { 'Idempotency-Key': dynamicRequestId },
body: {
device_id: dynamicDeviceId,
terminal_id: dynamicTerminalId,
amount: 32100,
currency: 'IDR',
request_id: dynamicRequestId
},
_label: 'POST /device/transactions/dynamic-qr duplicate'
});
if (dynamicQr?.data?.transaction_id !== dynamicQrReplay?.data?.transaction_id) {
throw new Error('dynamic QR idempotency returned a different transaction');
}
const dynamicCallback = {
partner_reference: dynamicQr?.data?.partner_reference,
partner_txn_id: `PTX-DYN-${ts}`,
amount: 32100,
currency: 'IDR',
payment_status: 'paid',
status: 'paid',
paid_at: new Date().toISOString()
};
const dynamicSignature = createHmac('sha256', SECRET).update(JSON.stringify(dynamicCallback)).digest('hex');
await req('/integrations/qris/callback', {
method: 'POST',
headers: { 'X-Partner-Signature': dynamicSignature },
body: { ...dynamicCallback, signature: dynamicSignature },
_label: 'POST /integrations/qris/callback dynamic-api'
});
await reqAdmin(`/admin/transactions/${dynamicQr?.data?.transaction_id}`, {
_label: 'GET /admin/transactions/:id dynamic-api'
});
const dueDynamicTx = await reqAdmin('/admin/transactions', {
method: 'POST',
body: {
partner_reference: `DUE-DYN-${ts}`,
merchant_id: merchantId,
outlet_id: dynamicOutletId,
terminal_id: dynamicTerminalId,
device_id: dynamicDeviceId,
amount: 12000,
currency: 'IDR',
qr_mode: 'dynamic',
initiation_mode: 'dynamic_api',
status: 'awaiting_payment',
expired_at: new Date(Date.now() - 60_000).toISOString()
},
_label: 'POST /admin/transactions due dynamic'
});
await reqAdmin('/admin/transactions/expire-due', {
method: 'POST',
body: { limit: 10 },
_label: 'POST /admin/transactions/expire-due'
});
await reqAdmin(`/admin/transactions/${dueDynamicTx?.data?.id}`, {
_label: 'GET /admin/transactions/:id expired dynamic'
});
const mqttOutlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
method: 'POST',
body: { name: `Dynamic MQTT Outlet ${ts}` },
_label: 'POST /admin/merchants/:id/outlets dynamic-mqtt'
});
const mqttOutletId = mqttOutlet?.data?.id;
const mqttTerminal = await reqAdmin(`/admin/outlets/${mqttOutletId}/terminals`, {
method: 'POST',
body: { terminal_code: `TERM-MQTT-${ts}`, qr_mode: 'dynamic_mqtt' },
_label: 'POST /admin/outlets/:id/terminals dynamic-mqtt'
});
const mqttTerminalId = mqttTerminal?.data?.id;
const mqttDevice = await reqAdmin('/admin/devices', {
method: 'POST',
body: {
device_code: `DEV-MQTT-${ts}`,
vendor: 'acme',
model: 'mqtt-v1',
communication_mode: 'mqtt',
capability_profile_json: {
dynamic_qr: { api_direct: false, mqtt: true },
flows: ['dynamic_qr:mqtt', 'static_payment_notification']
},
status: 'active'
},
_label: 'POST /admin/devices dynamic-mqtt'
});
const mqttDeviceId = mqttDevice?.data?.id;
await reqAdmin(`/admin/devices/${mqttDeviceId}/bind`, {
method: 'POST',
body: {
merchant_id: merchantId,
outlet_id: mqttOutletId,
terminal_id: mqttTerminalId
},
_label: 'POST /admin/devices/:id/bind dynamic-mqtt'
});
const mqttRequestId = `MQTT-DYN-${ts}`;
const mqttQr = await reqDevice('/device/mqtt/uplink/dynamic-qr/request', {
method: 'POST',
body: {
message_type: 'dynamic_qr_request',
request_id: mqttRequestId,
device_id: mqttDeviceId,
terminal_id: mqttTerminalId,
amount: 43200,
currency: 'IDR',
created_at: new Date().toISOString()
},
_label: 'POST /device/mqtt/uplink/dynamic-qr/request'
});
const mqttQrReplay = await reqDevice('/device/mqtt/uplink/dynamic-qr/request', {
method: 'POST',
body: {
message_type: 'dynamic_qr_request',
request_id: mqttRequestId,
device_id: mqttDeviceId,
terminal_id: mqttTerminalId,
amount: 43200,
currency: 'IDR',
created_at: new Date().toISOString()
},
_label: 'POST /device/mqtt/uplink/dynamic-qr/request duplicate'
});
if (mqttQr?.data?.transaction_id !== mqttQrReplay?.data?.transaction_id) {
throw new Error('MQTT dynamic QR idempotency returned a different transaction');
}
await reqAdmin(`/admin/devices/${mqttDeviceId}/mqtt-messages?correlation_id=${mqttRequestId}`, {
_label: 'GET /admin/devices/:id/mqtt-messages dynamic-mqtt'
});
const mqttCallback = {
partner_reference: mqttQr?.data?.partner_reference,
partner_txn_id: `PTX-MQTT-${ts}`,
amount: 43200,
currency: 'IDR',
payment_status: 'paid',
status: 'paid',
paid_at: new Date().toISOString()
};
const mqttSignature = createHmac('sha256', SECRET).update(JSON.stringify(mqttCallback)).digest('hex');
await req('/integrations/qris/callback', {
method: 'POST',
headers: { 'X-Partner-Signature': mqttSignature },
body: { ...mqttCallback, signature: mqttSignature },
_label: 'POST /integrations/qris/callback dynamic-mqtt'
});
await reqAdmin(`/admin/transactions/${mqttQr?.data?.transaction_id}`, {
_label: 'GET /admin/transactions/:id dynamic-mqtt'
});
const pushedConfig = await reqAdmin(`/admin/devices/${dynamicDeviceId}/config`, {
method: 'PATCH',
body: {
settings: {
volume: 65,
language: 'id-ID',
heartbeat_interval_seconds: 45
}
},
_label: 'PATCH /admin/devices/:id/config'
});
const configVersion = pushedConfig?.data?.config?.config_version;
await reqAdmin(`/admin/devices/${dynamicDeviceId}/config/status`, {
_label: 'GET /admin/devices/:id/config/status pending'
});
await reqAdmin(`/admin/devices/${dynamicDeviceId}/config/retry-push`, {
method: 'POST',
body: {},
_label: 'POST /admin/devices/:id/config/retry-push'
});
await reqDevice(`/device/config?device_id=${dynamicDeviceId}`, {
_label: 'GET /device/config'
});
await reqDevice('/device/config/ack', {
method: 'POST',
body: {
device_id: dynamicDeviceId,
config_version: configVersion,
status: 'applied',
result_payload: { applied_at: new Date().toISOString() }
},
_label: 'POST /device/config/ack'
});
await reqAdmin(`/admin/devices/${dynamicDeviceId}/config`, {
_label: 'GET /admin/devices/:id/config'
});
await reqAdmin(`/admin/devices/${dynamicDeviceId}/config/status`, {
_label: 'GET /admin/devices/:id/config/status applied'
});
await reqExpect(`/admin/devices/${dynamicDeviceId}/config/retry-push`, 409, {
method: 'POST',
headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
body: {},
_label: 'POST /admin/devices/:id/config/retry-push already applied'
});
await reqAdmin(`/admin/devices/${dynamicDeviceId}/mqtt-messages?message_type=config_push`, {
_label: 'GET /admin/devices/:id/mqtt-messages config'
});
await reqAdmin(`/admin/devices/${dynamicDeviceId}/mqtt-messages?message_type=config_ack`, {
_label: 'GET /admin/devices/:id/mqtt-messages config ack'
});
console.log(`Smoke point 4 flow done. tx=${txId} device=${deviceId}`);
})();