Add workspace support to Bird numbers API and update routes, methods, and tests accordingly

This commit is contained in:
Jeppe Bundgaard
2026-03-04 10:28:36 +01:00
parent b46d29c87f
commit 70b7f5de87
4 changed files with 103 additions and 12 deletions
+18
View File
@@ -305,6 +305,12 @@ paths:
summary: List your numbers
operationId: birdListNumbers
parameters:
- in: query
name: workspaceId
required: false
schema:
type: string
description: Bird Workspace identifier (optional if configured)
- in: query
name: page
required: false
@@ -331,6 +337,12 @@ paths:
summary: Get a number by ID
operationId: birdGetNumber
parameters:
- in: query
name: workspaceId
required: false
schema:
type: string
description: Bird Workspace identifier (optional if configured)
- in: path
name: id
required: true
@@ -350,6 +362,12 @@ paths:
summary: Delete/release a number by ID
operationId: birdDeleteNumber
parameters:
- in: query
name: workspaceId
required: false
schema:
type: string
description: Bird Workspace identifier (optional if configured)
- in: path
name: id
required: true
+12 -6
View File
@@ -296,16 +296,22 @@ class bird
return $this->sendPostRequest($base . '/' . rawurlencode($callId) . '/hangup', $payload);
}
public function listNumbers(array $query = []): array|object|null
public function listNumbers(string $workspaceId, array $query = []): array|object|null
{
$this->logBirdAction('BIRD_NUMBERS_LIST', 'query_keys=' . implode(',', array_keys($query)));
return $this->sendGetRequest('/numbers', $query);
$this->logBirdAction('BIRD_NUMBERS_LIST', 'workspace=' . $workspaceId . ' query_keys=' . implode(',', array_keys($query)));
return $this->sendGetRequest('/workspaces/' . rawurlencode($workspaceId) . '/numbers', $query);
}
public function getNumber(string $numberId): array|object|null
public function getNumber(string $workspaceId, string $numberId): array|object|null
{
$this->logBirdAction('BIRD_NUMBER_GET', 'number=' . $numberId);
return $this->sendGetRequest('/numbers/' . rawurlencode($numberId));
$this->logBirdAction('BIRD_NUMBER_GET', 'workspace=' . $workspaceId . ' number=' . $numberId);
return $this->sendGetRequest('/workspaces/' . rawurlencode($workspaceId) . '/numbers/' . rawurlencode($numberId));
}
public function deleteNumber(string $workspaceId, string $numberId): array|object|null
{
$this->logBirdAction('BIRD_NUMBER_DELETE', 'workspace=' . $workspaceId . ' number=' . $numberId);
return $this->sendDeleteRequest('/workspaces/' . rawurlencode($workspaceId) . '/numbers/' . rawurlencode($numberId));
}
public function createOutboundTestCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options = []): array
+56 -3
View File
@@ -9,6 +9,37 @@ class birdNumbersRoute
{
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 '';
}
public function run(): void
{
// List owned numbers
@@ -17,8 +48,16 @@ class birdNumbersRoute
// Permission: list numbers via Bird
self::requirePermission('modules_bird_numbers_list');
$client = new bird();
$ws = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ws === '') {
$response->error('Missing required parameter: workspaceId', 400);
}
$query = $this->getParametersAsArray();
$res = $client->listNumbers($query);
unset($query['workspaceId']);
$res = $client->listNumbers($ws, $query);
$response->success($res ?? []);
}, [
'modules_bird_numbers_list' => 'List your numbers via Bird',
@@ -33,7 +72,14 @@ class birdNumbersRoute
$response->error('Missing id', 400);
}
$client = new bird();
$res = $client->getNumber($id);
$ws = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ws === '') {
$response->error('Missing required parameter: workspaceId', 400);
}
$res = $client->getNumber($ws, $id);
$response->success($res ?? []);
}, [
'modules_bird_numbers_get' => 'Get a number by ID via Bird',
@@ -48,7 +94,14 @@ class birdNumbersRoute
$response->error('Missing id', 400);
}
$client = new bird();
$res = $client->sendDeleteRequest('/numbers/' . rawurlencode($id));
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ws === '') {
$response->error('Missing required parameter: workspaceId', 400);
}
$res = $client->deleteNumber($ws, $id);
$response->success($res ?? ['status' => 'ok']);
}, [
'modules_bird_numbers_delete' => 'Delete/release a number via Bird',
@@ -18,11 +18,13 @@ class DummyConfig2
public DummyVar2 $enabled;
public DummyVar2 $api_key;
public DummyVar2 $server_url;
public DummyVar2 $workplaceId;
public function __construct()
{
$this->enabled = new DummyVar2('true');
$this->api_key = new DummyVar2('test_api_key');
$this->server_url = new DummyVar2('https://example.test');
$this->workplaceId = new DummyVar2('test_workspace_id');
}
public function getModuleName(): string { return 'bird'; }
}
@@ -55,16 +57,28 @@ function assert_true2($cond, $msg)
}
}
// Test: list numbers (GET /numbers)
// Test: list numbers (GET /workspaces/{ws}/numbers)
$client = new FakeBird2();
$res = $client->sendGetRequest('/numbers', ['limit' => 10]);
$res = $client->listNumbers('test_workspace_id', ['limit' => 10]);
assert_true2(is_object($res) || is_array($res), 'Numbers response is JSON-decodable');
assert_true2(str_starts_with($client->last['url'], 'https://example.test/numbers'), 'Numbers GET URL composed correctly');
assert_true2(str_starts_with($client->last['url'], 'https://example.test/workspaces/test_workspace_id/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: 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: get number (GET /workspaces/{ws}/numbers/{id})
$clientNum = new FakeBird2();
$resNum = $clientNum->getNumber('test_workspace_id', 'num_123');
assert_true2(str_ends_with($clientNum->last['url'], '/workspaces/test_workspace_id/numbers/num_123'), 'Get number URL composed correctly');
assert_true2($clientNum->last['method'] === 'GET', 'HTTP method is GET (get number)');
// Test: delete number (DELETE /workspaces/{ws}/numbers/{id})
$clientDel = new FakeBird2();
$resDel = $clientDel->deleteNumber('test_workspace_id', 'num_123');
assert_true2(str_ends_with($clientDel->last['url'], '/workspaces/test_workspace_id/numbers/num_123'), 'Delete number URL composed correctly');
assert_true2($clientDel->last['method'] === 'DELETE', 'HTTP method is DELETE (delete number)');
// Test: create flash call (POST /voice/flash-calls)
$client2 = new FakeBird2();
$payload = [