Reapply "Update Bird module to use AccessKey authorization header instead of Bearer, improve error handling, and add support for workspace/channel configuration."
This reverts commit 1087ffb413.
This commit is contained in:
@@ -1,19 +1,27 @@
|
||||
## Elastic Stack credentials (used by Elasticsearch, Kibana, Filebeat, Metricbeat)
|
||||
# Choose a strong password; this value is used by all components in docker-compose.yml
|
||||
ELASTIC_PASSWORD=TrWsh_dev_EsP@55w0rd_2026!
|
||||
|
||||
# Kibana internal user password (kibana_system)
|
||||
KIBANA_PASSWORD=TrWsh_dev_Kbn@2026!
|
||||
|
||||
## Optional: Let’s Encrypt path for production (used by docker-compose.prod.yml)
|
||||
# Uncomment and set if needed on your host; default is /etc/letsencrypt on Linux
|
||||
# LETSENCRYPT_PATH=/etc/letsencrypt
|
||||
|
||||
## Fleet / Elastic Defend (generated below)
|
||||
# Service token for Fleet Server to authenticate to Elasticsearch
|
||||
FLEET_SERVER_SERVICE_TOKEN=AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL2ZsZWV0LXNlcnZlci10b2tlbjotbFlwZHRMeVNJYUJpd1ZKb19PQW5R
|
||||
# Enrollment token for Elastic Agent to enroll to Fleet (will be created after Kibana is ready)
|
||||
FLEET_ENROLLMENT_TOKEN=WXA0VVdad0Jlcmk3WHBCTE9ST2U6eUl3WC1CMGZUanVlV25HVm92MGpFQQ==
|
||||
|
||||
# APM
|
||||
APM_SECRET_TOKEN=apm_dev_token_2026!
|
||||
USE_ENV=true
|
||||
CONFIG_DB_DATABASE=nnks_db
|
||||
CONFIG_DB_HOST=94.130.142.41
|
||||
CONFIG_DB_PASSWORD=562X0Lrr7Cz6zpXZ11I
|
||||
CONFIG_DB_USER=root
|
||||
CONFIG_TIMEZONE=Europe/Copenhagen
|
||||
CORS=*
|
||||
DEBUG=false
|
||||
ECONOMIC_API_APP_ACCESS_GRANT=94bhkmdtaDA7kVn9abF2SGDccBDMvk5a6iWYnmJMbvQ1
|
||||
ECONOMIC_API_APP_ACCESS_GRANT2=qGSBSkh1pjBtdSOygHhaMPn1A4PcMto3sCDCGYpLmsg1
|
||||
ECONOMIC_API_APP_SECRET_TOKEN=V8GSEcIxMsTISczzTTBbOAMJyh8eucGZtBiGOxjMFg0
|
||||
EMAIL_WASH_CERTIFICATE_TOKEN=H7uDTtFaeN4asqpb5okh6dr8z209SGtt
|
||||
ENCRYPTION_KEY=Gvm37uF2VyTOjGkVl4kjrGQ0qRwOyq9lr3+p/QyUDjc\\=
|
||||
MINIO_ACCESS_KEY=d7u6RaFyYmckAIWYGUYr
|
||||
MINIO_ENDPOINT=http://162.55.225.220:9000
|
||||
MINIO_SECRET_KEY=a2wJUQfkOPNO3UJfXYIdpNq4r1RrthcjiUfW1gVS
|
||||
REDIS_CONFIG_DATABASE=0
|
||||
REDIS_CONFIG_HOST=redis
|
||||
REDIS_CONFIG_PASSWORD=BlVg5o1NwkkR1IjKxQm
|
||||
SLACK_DEFAULT_WEBHOOK=https://hooks.slaCk.com/services/T05SRKWTX9C/B08AGMP459P/1W5JN1NpHsHlbHHM2WljpvrU
|
||||
WORDPRESS_API_URL=https://www.truckwash.dk/wp-admin/admin-ajax.php
|
||||
WORDPRESS_STATIC_TOKEN=earm8BX4MFTgS6JCNQdqW5EzHUutv2Vx
|
||||
ELASTIC_APM_SERVER_URL=http://elastic-agent:8200
|
||||
ELASTIC_APM_SECRET_TOKEN=apm_dev_token
|
||||
ELASTIC_APM_SERVICE_NAME=api-truckwash
|
||||
ELASTIC_APM_ENVIRONMENT=dev
|
||||
AUTO_COMPOSER_INSTALL=false
|
||||
|
||||
+1
-1
@@ -7621,7 +7621,7 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
module: { type: string, enum: [bird] }
|
||||
variable: { type: string, enum: [api_key, enabled, server_url] }
|
||||
variable: { type: string, enum: [api_key, enabled, server_url, workplaceId, channelId] }
|
||||
type: { type: string, enum: [string, bool] }
|
||||
value:
|
||||
oneOf:
|
||||
|
||||
@@ -11,6 +11,7 @@ class bird
|
||||
{
|
||||
public const TEST_OUTBOUND_NUMBER_RAW = '+45 42 33 11 28';
|
||||
public const TEST_OUTBOUND_NUMBER_E164 = '+4542331128';
|
||||
private const ALLOWED_HANGUP_CAUSES = ['rejected', 'busy'];
|
||||
|
||||
/**
|
||||
* Configuration of the Bird module
|
||||
@@ -96,9 +97,9 @@ class bird
|
||||
$url = rtrim($this->config->server_url->getVariableValue(), '/') . $endpoint;
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->config->api_key->getVariableValue(),
|
||||
$this->buildAuthorizationHeader(),
|
||||
];
|
||||
$body = json_encode($data);
|
||||
$body = $this->encodeJsonBody($data);
|
||||
|
||||
$this->logBirdAction(
|
||||
'BIRD_HTTP_REQUEST',
|
||||
@@ -110,7 +111,7 @@ class bird
|
||||
$this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=POST endpoint=' . $endpoint . ' status=' . $status);
|
||||
if ($status >= 400) {
|
||||
$this->logBirdAction('BIRD_HTTP_ERROR', 'method=POST endpoint=' . $endpoint . ' status=' . $status, 0);
|
||||
throw new Exception('Bird API request failed with status ' . $status);
|
||||
throw new Exception($this->buildHttpErrorMessage($status, $response));
|
||||
}
|
||||
if ($response === '' || $response === false || $response === null) {
|
||||
return null;
|
||||
@@ -179,7 +180,7 @@ class bird
|
||||
}
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->config->api_key->getVariableValue(),
|
||||
$this->buildAuthorizationHeader(),
|
||||
];
|
||||
$this->logBirdAction(
|
||||
'BIRD_HTTP_REQUEST',
|
||||
@@ -191,7 +192,7 @@ class bird
|
||||
$this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=GET endpoint=' . $endpoint . ' status=' . $status);
|
||||
if ($status >= 400) {
|
||||
$this->logBirdAction('BIRD_HTTP_ERROR', 'method=GET endpoint=' . $endpoint . ' status=' . $status, 0);
|
||||
throw new Exception('Bird API request failed with status ' . $status);
|
||||
throw new Exception($this->buildHttpErrorMessage($status, $response));
|
||||
}
|
||||
if ($response === '' || $response === false || $response === null) {
|
||||
return null;
|
||||
@@ -209,9 +210,9 @@ class bird
|
||||
$url = rtrim($this->config->server_url->getVariableValue(), '/') . $endpoint;
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->config->api_key->getVariableValue(),
|
||||
$this->buildAuthorizationHeader(),
|
||||
];
|
||||
$body = json_encode($data);
|
||||
$body = $this->encodeJsonBody($data);
|
||||
|
||||
$this->logBirdAction(
|
||||
'BIRD_HTTP_REQUEST',
|
||||
@@ -223,7 +224,7 @@ class bird
|
||||
$this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=PATCH endpoint=' . $endpoint . ' status=' . $status);
|
||||
if ($status >= 400) {
|
||||
$this->logBirdAction('BIRD_HTTP_ERROR', 'method=PATCH endpoint=' . $endpoint . ' status=' . $status, 0);
|
||||
throw new Exception('Bird API request failed with status ' . $status);
|
||||
throw new Exception($this->buildHttpErrorMessage($status, $response));
|
||||
}
|
||||
if ($response === '' || $response === false || $response === null) {
|
||||
return null;
|
||||
@@ -246,7 +247,7 @@ class bird
|
||||
}
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->config->api_key->getVariableValue(),
|
||||
$this->buildAuthorizationHeader(),
|
||||
];
|
||||
$this->logBirdAction(
|
||||
'BIRD_HTTP_REQUEST',
|
||||
@@ -258,7 +259,7 @@ class bird
|
||||
$this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=DELETE endpoint=' . $endpoint . ' status=' . $status);
|
||||
if ($status >= 400) {
|
||||
$this->logBirdAction('BIRD_HTTP_ERROR', 'method=DELETE endpoint=' . $endpoint . ' status=' . $status, 0);
|
||||
throw new Exception('Bird API request failed with status ' . $status);
|
||||
throw new Exception($this->buildHttpErrorMessage($status, $response));
|
||||
}
|
||||
if ($response === '' || $response === false || $response === null) {
|
||||
return null;
|
||||
@@ -314,7 +315,8 @@ class bird
|
||||
$maxAttempts = (int)max(1, floor($maxPollSeconds / $pollIntervalSeconds));
|
||||
|
||||
$payload = $options;
|
||||
unset($payload['pollIntervalSeconds'], $payload['maxPollSeconds']);
|
||||
// Keep polling/hangup controls out of create-call payload to avoid Bird validation errors.
|
||||
unset($payload['pollIntervalSeconds'], $payload['maxPollSeconds'], $payload['hangupCause']);
|
||||
$payload['to'] = self::TEST_OUTBOUND_NUMBER_E164;
|
||||
|
||||
$this->logBirdAction(
|
||||
@@ -350,7 +352,10 @@ class bird
|
||||
if ($status !== null && in_array(strtolower($status), $acceptedStates, true)) {
|
||||
$hangupPayload = [];
|
||||
if (isset($options['hangupCause']) && is_string($options['hangupCause']) && $options['hangupCause'] !== '') {
|
||||
$hangupPayload['cause'] = $options['hangupCause'];
|
||||
$normalizedCause = strtolower(trim($options['hangupCause']));
|
||||
if (in_array($normalizedCause, self::ALLOWED_HANGUP_CAUSES, true)) {
|
||||
$hangupPayload['cause'] = $normalizedCause;
|
||||
}
|
||||
}
|
||||
$hangupResponse = $this->hangupVoiceCall($workspaceId, $channelId, $callId, $hangupPayload);
|
||||
$this->logBirdAction('BIRD_TEST_OUTBOUND_CALL_HANGUP_SENT', 'call=' . $callId . ' status=' . $status);
|
||||
@@ -416,6 +421,71 @@ class bird
|
||||
return null;
|
||||
}
|
||||
|
||||
private function buildHttpErrorMessage(int $status, string|false|null $response): string
|
||||
{
|
||||
$base = 'Bird API request failed with status ' . $status;
|
||||
if (!is_string($response) || trim($response) === '') {
|
||||
return $base;
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
if (is_array($decoded)) {
|
||||
$details = [];
|
||||
foreach (['message', 'error', 'description'] as $key) {
|
||||
if (isset($decoded[$key]) && is_string($decoded[$key]) && trim($decoded[$key]) !== '') {
|
||||
$details[] = trim($decoded[$key]);
|
||||
}
|
||||
}
|
||||
if (isset($decoded['errors'])) {
|
||||
if (is_string($decoded['errors']) && trim($decoded['errors']) !== '') {
|
||||
$details[] = trim($decoded['errors']);
|
||||
} elseif (is_array($decoded['errors'])) {
|
||||
foreach ($decoded['errors'] as $error) {
|
||||
if (is_string($error) && trim($error) !== '') {
|
||||
$details[] = trim($error);
|
||||
} elseif (is_array($error) && isset($error['message']) && is_string($error['message']) && trim($error['message']) !== '') {
|
||||
$details[] = trim($error['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$details = array_values(array_unique(array_filter($details)));
|
||||
if (!empty($details)) {
|
||||
return $base . ': ' . implode(' | ', $details);
|
||||
}
|
||||
}
|
||||
|
||||
$snippet = preg_replace('/\s+/', ' ', trim($response));
|
||||
if (!is_string($snippet) || $snippet === '') {
|
||||
return $base;
|
||||
}
|
||||
if (strlen($snippet) > 220) {
|
||||
$snippet = substr($snippet, 0, 220) . '...';
|
||||
}
|
||||
return $base . ': ' . $snippet;
|
||||
}
|
||||
|
||||
private function buildAuthorizationHeader(): string
|
||||
{
|
||||
$apiKey = trim((string)$this->config->api_key->getVariableValue());
|
||||
if ($apiKey === '') {
|
||||
return 'Authorization: AccessKey';
|
||||
}
|
||||
if (preg_match('/^(Bearer|AccessKey)\s+/i', $apiKey) === 1) {
|
||||
return 'Authorization: ' . $apiKey;
|
||||
}
|
||||
return 'Authorization: AccessKey ' . $apiKey;
|
||||
}
|
||||
|
||||
private function encodeJsonBody(array $data): string
|
||||
{
|
||||
// Bird control endpoints expect an object payload; use {} for empty body.
|
||||
if ($data === []) {
|
||||
return '{}';
|
||||
}
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
private function logBirdAction(string $action, string $message, int $type = 1): void
|
||||
{
|
||||
try {
|
||||
@@ -437,3 +507,6 @@ class bird
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@ require_once WD . '/traits/module_config_t.php';
|
||||
require_once WD . '/modules/bird/config/bird_enabled_c.php';
|
||||
require_once WD . '/modules/bird/config/bird_api_key_c.php';
|
||||
require_once WD . '/modules/bird/config/bird_server_url_c.php';
|
||||
require_once WD . '/modules/bird/config/bird_workplaceId_c.php';
|
||||
require_once WD . '/modules/bird/config/bird_channelId_c.php';
|
||||
|
||||
use bird\config\bird_enabled_c;
|
||||
use bird\config\bird_api_key_c;
|
||||
use bird\config\bird_server_url_c;
|
||||
use bird\config\bird_workplaceId_c;
|
||||
use bird\config\bird_channelId_c;
|
||||
use traits\module_config_t;
|
||||
|
||||
class bird_c
|
||||
@@ -34,6 +38,18 @@ class bird_c
|
||||
*/
|
||||
public bird_server_url_c $server_url;
|
||||
|
||||
/**
|
||||
* Default Bird workplace identifier
|
||||
* @var bird_workplaceId_c
|
||||
*/
|
||||
public bird_workplaceId_c $workplaceId;
|
||||
|
||||
/**
|
||||
* Default Bird channel identifier
|
||||
* @var bird_channelId_c
|
||||
*/
|
||||
public bird_channelId_c $channelId;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfig('bird');
|
||||
@@ -41,9 +57,13 @@ class bird_c
|
||||
bird_enabled_c::class,
|
||||
bird_api_key_c::class,
|
||||
bird_server_url_c::class,
|
||||
bird_workplaceId_c::class,
|
||||
bird_channelId_c::class,
|
||||
]);
|
||||
$this->enabled = new bird_enabled_c();
|
||||
$this->api_key = new bird_api_key_c();
|
||||
$this->server_url = new bird_server_url_c();
|
||||
$this->workplaceId = new bird_workplaceId_c();
|
||||
$this->channelId = new bird_channelId_c();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,49 @@ class birdVoiceCallsRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
private function normalizeOptionalString(mixed $value): string
|
||||
{
|
||||
if (!is_scalar($value)) {
|
||||
return '';
|
||||
}
|
||||
$normalized = trim((string)$value);
|
||||
if ($normalized === '') {
|
||||
return '';
|
||||
}
|
||||
$lower = strtolower($normalized);
|
||||
if ($lower === 'undefined' || $lower === 'null') {
|
||||
return '';
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function getConfiguredWorkspaceId(bird $client): string
|
||||
{
|
||||
$workspaceConfig = null;
|
||||
if (property_exists($client->config, 'workspaceId')) {
|
||||
$workspaceConfig = $client->config->workspaceId;
|
||||
} elseif (property_exists($client->config, 'workplaceId')) {
|
||||
// Backward compatibility with existing config key naming.
|
||||
$workspaceConfig = $client->config->workplaceId;
|
||||
}
|
||||
if (is_object($workspaceConfig) && method_exists($workspaceConfig, 'getVariableValue')) {
|
||||
return $this->normalizeOptionalString($workspaceConfig->getVariableValue());
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function getConfiguredChannelId(bird $client): string
|
||||
{
|
||||
$channelConfig = null;
|
||||
if (property_exists($client->config, 'channelId')) {
|
||||
$channelConfig = $client->config->channelId;
|
||||
}
|
||||
if (is_object($channelConfig) && method_exists($channelConfig, 'getVariableValue')) {
|
||||
return $this->normalizeOptionalString($channelConfig->getVariableValue());
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// Create a voice call
|
||||
@@ -18,8 +61,8 @@ class birdVoiceCallsRoute
|
||||
self::requirePermission('modules_bird_voice_calls_create');
|
||||
$client = new bird();
|
||||
// Require workspace/channel per Bird API docs
|
||||
$ws = (string)($this->fromRequest('workspaceId') ?? '');
|
||||
$ch = (string)($this->fromRequest('channelId') ?? '');
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId'));
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
@@ -37,8 +80,8 @@ class birdVoiceCallsRoute
|
||||
// Permission: list voice calls via Bird
|
||||
self::requirePermission('modules_bird_voice_calls_list');
|
||||
$client = new bird();
|
||||
$ws = (string)($this->fromQuery('workspaceId') ?? '');
|
||||
$ch = (string)($this->fromQuery('channelId') ?? '');
|
||||
$ws = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromQuery('channelId'));
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
@@ -60,8 +103,8 @@ class birdVoiceCallsRoute
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = (string)($this->fromQuery('workspaceId') ?? '');
|
||||
$ch = (string)($this->fromQuery('channelId') ?? '');
|
||||
$ws = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromQuery('channelId'));
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
@@ -81,12 +124,17 @@ class birdVoiceCallsRoute
|
||||
if ($id === null || $id === '') {
|
||||
$response->error('Missing id', 400);
|
||||
}
|
||||
$ws = (string)($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId') ?? '');
|
||||
$ch = (string)($this->fromRequest('channelId') ?? $this->fromQuery('channelId') ?? '');
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
$res = $client->hangupVoiceCall($ws, $ch, $id);
|
||||
$payload = [];
|
||||
$cause = $this->normalizeOptionalString($this->fromRequest('cause') ?? $this->fromQuery('cause'));
|
||||
if ($cause !== '') {
|
||||
$payload['cause'] = $cause;
|
||||
}
|
||||
$res = $client->hangupVoiceCall($ws, $ch, $id, $payload);
|
||||
$response->success($res ?? ['status' => 'ok']);
|
||||
}, [
|
||||
'modules_bird_voice_calls_hangup' => 'Hang up a voice call by ID via Bird',
|
||||
@@ -98,8 +146,14 @@ class birdVoiceCallsRoute
|
||||
self::requirePermission('modules_bird_voice_calls_test_outbound');
|
||||
$client = new bird();
|
||||
|
||||
$ws = (string)($this->fromRequest('workspaceId') ?? '');
|
||||
$ch = (string)($this->fromRequest('channelId') ?? '');
|
||||
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
||||
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
||||
if ($ws === '') {
|
||||
$ws = $this->getConfiguredWorkspaceId($client);
|
||||
}
|
||||
if ($ch === '') {
|
||||
$ch = $this->getConfiguredChannelId($client);
|
||||
}
|
||||
if ($ws === '' || $ch === '') {
|
||||
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||
}
|
||||
@@ -107,8 +161,12 @@ class birdVoiceCallsRoute
|
||||
$payload = $this->getParametersAsArray();
|
||||
unset($payload['workspaceId'], $payload['channelId']);
|
||||
|
||||
$result = $client->createOutboundTestCallAndHangupWhenAccepted($ws, $ch, $payload);
|
||||
$response->success($result);
|
||||
try {
|
||||
$result = $client->createOutboundTestCallAndHangupWhenAccepted($ws, $ch, $payload);
|
||||
$response->success($result);
|
||||
} catch (\Throwable $e) {
|
||||
$response->error($e->getMessage(), 500);
|
||||
}
|
||||
}, [
|
||||
'modules_bird_voice_calls_test_outbound' => 'Place a test outbound call and hang up when accepted',
|
||||
]);
|
||||
|
||||
@@ -45,10 +45,10 @@ if (($bird->last['url'] ?? '') === $expectedUrl) { ok('URL is correctly composed
|
||||
$headers = $bird->last['headers'] ?? [];
|
||||
$hasAuth = false; $hasJson = false;
|
||||
foreach ($headers as $h) {
|
||||
if (stripos($h, 'authorization:') === 0 && str_contains($h, 'Bearer secret_token')) { $hasAuth = true; }
|
||||
if (stripos($h, 'authorization:') === 0 && str_contains($h, 'AccessKey secret_token')) { $hasAuth = true; }
|
||||
if (strcasecmp($h, 'Content-Type: application/json') === 0) { $hasJson = true; }
|
||||
}
|
||||
if ($hasAuth) { ok('Authorization header includes Bearer token'); } else { fail('Missing or invalid Authorization header'); }
|
||||
if ($hasAuth) { ok('Authorization header includes AccessKey token'); } else { fail('Missing or invalid Authorization header'); }
|
||||
if ($hasJson) { ok('Content-Type header is application/json'); } else { fail('Missing Content-Type: application/json header'); }
|
||||
|
||||
$expectedBody = json_encode($payload);
|
||||
|
||||
@@ -62,7 +62,7 @@ assert_true2(is_object($res) || is_array($res), 'Numbers response is JSON-decoda
|
||||
assert_true2(str_starts_with($client->last['url'], 'https://example.test/numbers'), 'Numbers GET URL composed correctly');
|
||||
assert_true2(str_contains($client->last['url'], 'limit=10'), 'Query string encoded correctly');
|
||||
assert_true2($client->last['method'] === 'GET', 'HTTP method is GET (numbers)');
|
||||
assert_true2(in_array('Authorization: Bearer test_api_key', $client->last['headers'], true), 'Authorization header is set (numbers)');
|
||||
assert_true2(in_array('Authorization: AccessKey test_api_key', $client->last['headers'], true), 'Authorization header is set (numbers)');
|
||||
assert_true2(empty($client->last['body']), 'GET body is empty (numbers)');
|
||||
|
||||
// Test: create flash call (POST /voice/flash-calls)
|
||||
@@ -76,7 +76,7 @@ $res2 = $client2->sendPostRequest('/voice/flash-calls', $payload);
|
||||
assert_true2(is_object($res2) || is_array($res2), 'Flash call response is JSON-decodable');
|
||||
assert_true2(str_starts_with($client2->last['url'], 'https://example.test/voice/flash-calls'), 'Flash POST URL composed correctly');
|
||||
assert_true2($client2->last['method'] === 'POST', 'HTTP method is POST (flash)');
|
||||
assert_true2(in_array('Authorization: Bearer test_api_key', $client2->last['headers'], true), 'Authorization header is set (flash)');
|
||||
assert_true2(in_array('Authorization: AccessKey test_api_key', $client2->last['headers'], true), 'Authorization header is set (flash)');
|
||||
assert_true2(json_decode($client2->last['body'], true)['code'] === '1234', 'Flash POST body encoded correctly');
|
||||
|
||||
// Test: list flash calls (GET /voice/flash-calls)
|
||||
@@ -109,3 +109,4 @@ assert_true2($client6->last['method'] === 'POST', 'HTTP method is POST (flash en
|
||||
assert_true2(json_decode($client6->last['body'], true)['from'] === '+4599988877', 'Flash end-by-numbers body encoded correctly');
|
||||
|
||||
echo "NumbersAndFlashCallsApiTest completed.\n";
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ $res = $client->sendPostRequest('/voice/calls', $payload);
|
||||
assert_true(is_object($res) || is_array($res), 'Response is JSON-decodable');
|
||||
assert_true(str_starts_with($client->last['url'], 'https://example.test/voice/calls'), 'POST URL composed correctly');
|
||||
assert_true($client->last['method'] === 'POST', 'HTTP method is POST');
|
||||
assert_true(in_array('Authorization: Bearer test_api_key', $client->last['headers'], true), 'Authorization header is set');
|
||||
assert_true(in_array('Authorization: AccessKey test_api_key', $client->last['headers'], true), 'Authorization header is set');
|
||||
assert_true(json_decode($client->last['body'], true)['to'] === '+4511122233', 'POST body encoded correctly');
|
||||
|
||||
// Test: get call (GET /voice/calls/{id})
|
||||
@@ -76,4 +76,12 @@ assert_true(str_starts_with($client2->last['url'], 'https://example.test/voice/c
|
||||
assert_true($client2->last['method'] === 'GET', 'HTTP method is GET');
|
||||
assert_true(empty($client2->last['body']), 'GET body is empty');
|
||||
|
||||
// Test: hangup with empty payload encodes an object body ({}), not array body ([]).
|
||||
$client3 = new FakeBird();
|
||||
$client3->hangupVoiceCall('ws_123', 'ch_123', 'call_123');
|
||||
assert_true(str_ends_with($client3->last['url'], '/workspaces/ws_123/channels/ch_123/calls/call_123/hangup'), 'Hangup URL composed correctly');
|
||||
assert_true($client3->last['method'] === 'POST', 'Hangup uses POST');
|
||||
assert_true(trim((string)$client3->last['body']) === '{}', 'Hangup empty body is encoded as JSON object');
|
||||
|
||||
echo "VoiceCallsApiTest completed.\n";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user