207 lines
6.9 KiB
PHP
207 lines
6.9 KiB
PHP
<?php
|
|
|
|
app_require('classes/bird.php');
|
|
|
|
use classes\bird;
|
|
|
|
class BirdGateCallConfigValueFake
|
|
{
|
|
public function __construct(private readonly mixed $value) {}
|
|
|
|
public function getVariableValue(): mixed
|
|
{
|
|
return $this->value;
|
|
}
|
|
}
|
|
|
|
class BirdGateCallClientFake extends bird
|
|
{
|
|
/** @var string[] */
|
|
public array $statusQueue = [];
|
|
/** @var string[] */
|
|
public array $statusesSeen = [];
|
|
public int $hangupCalls = 0;
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $hangupPayloads = [];
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $createPayloads = [];
|
|
|
|
/**
|
|
* @param string[] $statusQueue
|
|
*/
|
|
public function __construct(mixed $workspaceId = 'workspace_1', mixed $channelId = 'channel_1', array $statusQueue = ['accepted'])
|
|
{
|
|
$this->statusQueue = $statusQueue;
|
|
$this->config = (object)[
|
|
'workspaceId' => new BirdGateCallConfigValueFake($workspaceId),
|
|
'workplaceId' => new BirdGateCallConfigValueFake($workspaceId),
|
|
'channelId' => new BirdGateCallConfigValueFake($channelId),
|
|
];
|
|
}
|
|
|
|
public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null
|
|
{
|
|
$this->createPayloads[] = [
|
|
'workspaceId' => $workspaceId,
|
|
'channelId' => $channelId,
|
|
'payload' => $payload,
|
|
];
|
|
return ['id' => 'call_123'];
|
|
}
|
|
|
|
public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null
|
|
{
|
|
$status = array_shift($this->statusQueue);
|
|
if (!is_string($status) || trim($status) === '') {
|
|
$status = 'ringing';
|
|
}
|
|
$this->statusesSeen[] = $status;
|
|
|
|
return [
|
|
'id' => $callId,
|
|
'status' => $status,
|
|
];
|
|
}
|
|
|
|
public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
|
|
{
|
|
$this->hangupCalls++;
|
|
$this->hangupPayloads[] = $payload;
|
|
return ['status' => 'completed'];
|
|
}
|
|
|
|
protected function waitForCallPollInterval(int $pollIntervalSeconds): void
|
|
{
|
|
// Avoid real sleeps in tests.
|
|
}
|
|
}
|
|
|
|
it('fails fast when workspace id is missing for gate calls', function (): void {
|
|
$client = new BirdGateCallClientFake(workspaceId: '', channelId: 'channel_1');
|
|
|
|
expect(fn() => $client->callGateAndHangupWhenAccepted(45, 12345678, 10))
|
|
->toThrow(\Exception::class, 'Bird workspaceId is not configured for gate calls');
|
|
});
|
|
|
|
it('fails fast when channel id is missing for gate calls', function (): void {
|
|
$client = new BirdGateCallClientFake(workspaceId: 'workspace_1', channelId: '');
|
|
|
|
expect(fn() => $client->callGateAndHangupWhenAccepted(45, 12345678, 10))
|
|
->toThrow(\Exception::class, 'Bird channelId is not configured for gate calls');
|
|
});
|
|
|
|
it('fails with terminal status details when call never reaches accepted state', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['busy']
|
|
);
|
|
|
|
try {
|
|
$client->callGateAndHangupWhenAccepted(45, 12345678, 10);
|
|
$thrown = null;
|
|
} catch (\Exception $e) {
|
|
$thrown = $e;
|
|
}
|
|
|
|
expect($thrown)->toBeInstanceOf(\Exception::class);
|
|
expect($thrown?->getMessage())->toContain('terminal status: busy');
|
|
expect($thrown?->getMessage())->not->toContain('Bird API request failed');
|
|
expect($client->hangupCalls)->toBe(0);
|
|
});
|
|
|
|
it('hangs up when call reaches accepted state', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['accepted']
|
|
);
|
|
|
|
$client->callGateAndHangupWhenAccepted(45, 12345678, 10);
|
|
|
|
expect($client->hangupCalls)->toBe(1);
|
|
expect($client->createPayloads)->toHaveCount(1);
|
|
expect($client->createPayloads[0]['workspaceId'])->toBe('workspace_1');
|
|
expect($client->createPayloads[0]['channelId'])->toBe('channel_1');
|
|
expect($client->createPayloads[0]['payload']['to'])->toBe('+4512345678');
|
|
expect($client->createPayloads[0]['payload']['ringTimeout'])->toBe(10);
|
|
expect(array_key_exists('timeout', $client->createPayloads[0]['payload']))->toBeFalse();
|
|
expect($client->hangupPayloads[0])->toBe([]);
|
|
});
|
|
|
|
it('hangs up immediately when status transitions to accepted', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['ringing', 'accepted', 'ongoing']
|
|
);
|
|
|
|
$client->callGateAndHangupWhenAccepted(45, 12345678, 10);
|
|
|
|
expect($client->hangupCalls)->toBe(1);
|
|
expect($client->statusesSeen)->toBe(['ringing', 'accepted']);
|
|
});
|
|
|
|
it('maps legacy timeout option to documented ringTimeout payload field', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['accepted']
|
|
);
|
|
|
|
$client->createOutboundTestCallAndHangupWhenAccepted('workspace_1', 'channel_1', [
|
|
'to' => '+4512345678',
|
|
'timeout' => 12,
|
|
]);
|
|
|
|
expect($client->createPayloads)->toHaveCount(1);
|
|
expect($client->createPayloads[0]['payload']['ringTimeout'])->toBe(12);
|
|
expect(array_key_exists('timeout', $client->createPayloads[0]['payload']))->toBeFalse();
|
|
});
|
|
|
|
it('clamps derived ringTimeout to Bird documented max when gate timeout is high', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['accepted']
|
|
);
|
|
|
|
$client->callGateAndHangupWhenAccepted(45, 12345678, 1000);
|
|
|
|
expect($client->createPayloads)->toHaveCount(1);
|
|
expect($client->createPayloads[0]['payload']['ringTimeout'])->toBe(120);
|
|
expect(array_key_exists('timeout', $client->createPayloads[0]['payload']))->toBeFalse();
|
|
});
|
|
|
|
it('passes documented hangup cause when provided', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['accepted']
|
|
);
|
|
|
|
$client->createOutboundTestCallAndHangupWhenAccepted('workspace_1', 'channel_1', [
|
|
'to' => '+4512345678',
|
|
'hangupCause' => 'rejected',
|
|
]);
|
|
|
|
expect($client->hangupCalls)->toBe(1);
|
|
expect($client->hangupPayloads[0]['cause'])->toBe('rejected');
|
|
});
|
|
|
|
it('drops unsupported hangup cause values from request payload', function (): void {
|
|
$client = new BirdGateCallClientFake(
|
|
workspaceId: 'workspace_1',
|
|
channelId: 'channel_1',
|
|
statusQueue: ['accepted']
|
|
);
|
|
|
|
$client->createOutboundTestCallAndHangupWhenAccepted('workspace_1', 'channel_1', [
|
|
'to' => '+4512345678',
|
|
'hangupCause' => 'completed',
|
|
]);
|
|
|
|
expect($client->hangupCalls)->toBe(1);
|
|
expect($client->hangupPayloads[0])->toBe([]);
|
|
});
|