diff --git a/src/AccessGridClient.php b/src/AccessGridClient.php index 3bce409..f5daca1 100644 --- a/src/AccessGridClient.php +++ b/src/AccessGridClient.php @@ -75,7 +75,7 @@ public function makeRequest(string $method, string $endpoint, ?array $data = nul // Extract resource ID from the endpoint if needed for signature $resourceId = null; - if ($method === 'GET' || ($method === 'POST' && (empty($data) || $data === []))) { + if ($method === 'GET' || $method === 'DELETE' || ($method === 'POST' && (empty($data) || $data === []))) { // Extract the ID from the endpoint - patterns like /resource/{id} or /resource/{id}/action $parts = array_filter(explode('/', trim($endpoint, '/'))); if (count($parts) >= 2) { @@ -93,7 +93,7 @@ public function makeRequest(string $method, string $endpoint, ?array $data = nul // Special handling for requests with no payload: // 1. POST requests with empty body (like unlink/suspend/resume) // 2. GET requests - if (($method === 'POST' && empty($data)) || $method === 'GET') { + if (($method === 'POST' && empty($data)) || $method === 'GET' || $method === 'DELETE') { // For these requests, use {"id": "card_id"} as the payload for signature generation if ($resourceId) { $payload = json_encode(['id' => $resourceId]); @@ -117,7 +117,7 @@ public function makeRequest(string $method, string $endpoint, ?array $data = nul // For requests with empty bodies (GET or action endpoints like unlink/suspend/resume), // we need to include the sig_payload parameter - if ($method === 'GET' || ($method === 'POST' && empty($data))) { + if ($method === 'GET' || $method === 'DELETE' || ($method === 'POST' && empty($data))) { if ($params === null) { $params = []; } @@ -156,6 +156,10 @@ public function makeRequest(string $method, string $endpoint, ?array $data = nul throw new AccessGridException('API request failed: ' . $errorMessage); } + if ($responseBody === '' || $responseBody === null) { + return []; + } + $decoded = json_decode($responseBody, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new AccessGridException('Invalid JSON response: ' . json_last_error_msg()); @@ -183,4 +187,9 @@ public function patch(string $endpoint, array $data): array { return $this->makeRequest('PATCH', $endpoint, $data); } + + public function delete(string $endpoint): array + { + return $this->makeRequest('DELETE', $endpoint); + } } \ No newline at end of file diff --git a/src/Models/Webhook.php b/src/Models/Webhook.php new file mode 100644 index 0000000..e0a01b7 --- /dev/null +++ b/src/Models/Webhook.php @@ -0,0 +1,33 @@ +client = $client; + $this->id = $data['id'] ?? null; + $this->name = $data['name'] ?? null; + $this->url = $data['url'] ?? null; + $this->authMethod = $data['auth_method'] ?? null; + $this->subscribedEvents = $data['subscribed_events'] ?? []; + $this->createdAt = $data['created_at'] ?? null; + $this->privateKey = $data['private_key'] ?? null; + $this->clientCert = $data['client_cert'] ?? null; + $this->certExpiresAt = $data['cert_expires_at'] ?? null; + } +} diff --git a/src/Services/Console.php b/src/Services/Console.php index 0def75f..a07a0f3 100644 --- a/src/Services/Console.php +++ b/src/Services/Console.php @@ -6,6 +6,7 @@ use AccessGrid\Models\Template; use AccessGrid\Models\PassTemplatePair; use AccessGrid\Models\LedgerItem; +use AccessGrid\Models\Webhook; class Console { @@ -106,6 +107,40 @@ public function listLedgerItems(array $params = []): array return $response; } + /** + * List webhooks + */ + public function listWebhooks(array $params = []): array + { + $response = $this->client->get('/v1/console/webhooks', $params); + + if (isset($response['webhooks'])) { + $response['webhooks'] = array_map( + fn($wh) => new Webhook($this->client, $wh), + $response['webhooks'] + ); + } + + return $response; + } + + /** + * Create a webhook + */ + public function createWebhook(array $data): Webhook + { + $response = $this->client->post('/v1/console/webhooks', $data); + return new Webhook($this->client, $response); + } + + /** + * Delete a webhook + */ + public function deleteWebhook(string $webhookId): void + { + $this->client->delete("/v1/console/webhooks/{$webhookId}"); + } + /** * Get iOS provisioning identifiers for preflight */ diff --git a/tests/Models/WebhookTest.php b/tests/Models/WebhookTest.php new file mode 100644 index 0000000..ec2b8c7 --- /dev/null +++ b/tests/Models/WebhookTest.php @@ -0,0 +1,82 @@ +client, [ + 'id' => 'wh_abc123', + 'name' => 'My Webhook', + 'url' => 'https://example.com/webhook', + 'auth_method' => 'bearer_token', + 'subscribed_events' => ['ag.access_pass.issued', 'ag.access_pass.activated'], + 'created_at' => '2025-06-01T12:00:00Z', + 'private_key' => 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', + ]); + + $this->assertEquals('wh_abc123', $webhook->id); + $this->assertEquals('My Webhook', $webhook->name); + $this->assertEquals('https://example.com/webhook', $webhook->url); + $this->assertEquals('bearer_token', $webhook->authMethod); + $this->assertCount(2, $webhook->subscribedEvents); + $this->assertEquals('ag.access_pass.issued', $webhook->subscribedEvents[0]); + $this->assertEquals('2025-06-01T12:00:00Z', $webhook->createdAt); + $this->assertEquals('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', $webhook->privateKey); + $this->assertNull($webhook->clientCert); + $this->assertNull($webhook->certExpiresAt); + } + + public function testConstructWithMtls(): void + { + $webhook = new Webhook($this->client, [ + 'id' => 'wh_def456', + 'name' => 'mTLS Webhook', + 'url' => 'https://secure.example.com/webhook', + 'auth_method' => 'mtls', + 'subscribed_events' => ['ag.card_template.created'], + 'created_at' => '2025-06-01T12:00:00Z', + 'client_cert' => '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----', + 'cert_expires_at' => '2025-12-01T12:00:00Z', + ]); + + $this->assertEquals('wh_def456', $webhook->id); + $this->assertEquals('mtls', $webhook->authMethod); + $this->assertEquals('-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----', $webhook->clientCert); + $this->assertEquals('2025-12-01T12:00:00Z', $webhook->certExpiresAt); + $this->assertNull($webhook->privateKey); + } + + public function testConstructWithNullName(): void + { + $webhook = new Webhook($this->client, [ + 'id' => 'wh_ghi789', + 'name' => null, + 'url' => 'https://example.com/hook', + 'auth_method' => 'bearer_token', + 'subscribed_events' => ['ag.access_pass.issued'], + 'created_at' => '2025-06-01T12:00:00Z', + ]); + + $this->assertNull($webhook->name); + } + + public function testConstructWithMinimalData(): void + { + $webhook = new Webhook($this->client, []); + + $this->assertNull($webhook->id); + $this->assertNull($webhook->name); + $this->assertNull($webhook->url); + $this->assertNull($webhook->authMethod); + $this->assertEquals([], $webhook->subscribedEvents); + $this->assertNull($webhook->createdAt); + $this->assertNull($webhook->privateKey); + $this->assertNull($webhook->clientCert); + $this->assertNull($webhook->certExpiresAt); + } +} diff --git a/tests/Services/ConsoleTest.php b/tests/Services/ConsoleTest.php index 38472a6..e83ea62 100644 --- a/tests/Services/ConsoleTest.php +++ b/tests/Services/ConsoleTest.php @@ -9,6 +9,7 @@ use AccessGrid\Models\LedgerItem; use AccessGrid\Models\LedgerItemAccessPass; use AccessGrid\Models\LedgerItemPassTemplate; +use AccessGrid\Models\Webhook; class ConsoleTest extends TestCase { @@ -402,4 +403,153 @@ public function testListLedgerItemsEmpty(): void $this->assertCount(0, $result['ledger_items']); } + + // --- Webhooks --- + + public function testListWebhooks(): void + { + $this->expectRequest('GET', '/v1/console/webhooks', 200, [ + 'webhooks' => [ + [ + 'id' => 'wh_abc123', + 'name' => 'My Webhook', + 'url' => 'https://example.com/webhook', + 'auth_method' => 'bearer_token', + 'subscribed_events' => ['ag.access_pass.issued', 'ag.access_pass.activated'], + 'created_at' => '2025-06-01T12:00:00Z', + ], + [ + 'id' => 'wh_def456', + 'name' => 'mTLS Webhook', + 'url' => 'https://secure.example.com/webhook', + 'auth_method' => 'mtls', + 'subscribed_events' => ['ag.card_template.created'], + 'created_at' => '2025-06-02T12:00:00Z', + 'cert_expires_at' => '2025-12-02T12:00:00Z', + ], + ], + 'pagination' => [ + 'current_page' => 1, + 'per_page' => 50, + 'total_pages' => 1, + 'total_count' => 2, + ], + ]); + + $result = $this->client->console->listWebhooks(); + + $this->assertArrayHasKey('webhooks', $result); + $this->assertArrayHasKey('pagination', $result); + $this->assertCount(2, $result['webhooks']); + + $wh = $result['webhooks'][0]; + $this->assertInstanceOf(Webhook::class, $wh); + $this->assertEquals('wh_abc123', $wh->id); + $this->assertEquals('My Webhook', $wh->name); + $this->assertEquals('bearer_token', $wh->authMethod); + $this->assertCount(2, $wh->subscribedEvents); + + $wh2 = $result['webhooks'][1]; + $this->assertInstanceOf(Webhook::class, $wh2); + $this->assertEquals('mtls', $wh2->authMethod); + $this->assertEquals('2025-12-02T12:00:00Z', $wh2->certExpiresAt); + + $this->assertEquals(1, $result['pagination']['current_page']); + $this->assertEquals(2, $result['pagination']['total_count']); + } + + public function testListWebhooksWithPagination(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('GET'), + $this->callback(function (string $url) { + return strpos($url, '/v1/console/webhooks') !== false + && strpos($url, 'page=2') !== false + && strpos($url, 'per_page=10') !== false; + }), + $this->anything(), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, json_encode([ + 'webhooks' => [], + 'pagination' => ['current_page' => 2, 'per_page' => 10, 'total_pages' => 3, 'total_count' => 25], + ]))); + + $result = $this->client->console->listWebhooks(['page' => 2, 'per_page' => 10]); + + $this->assertCount(0, $result['webhooks']); + $this->assertEquals(2, $result['pagination']['current_page']); + } + + public function testCreateWebhookBearerToken(): void + { + $this->expectRequest('POST', '/v1/console/webhooks', 201, [ + 'id' => 'wh_new123', + 'name' => 'New Webhook', + 'url' => 'https://example.com/hook', + 'auth_method' => 'bearer_token', + 'subscribed_events' => ['ag.access_pass.issued'], + 'created_at' => '2025-06-15T10:00:00Z', + 'private_key' => 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', + ]); + + $webhook = $this->client->console->createWebhook([ + 'name' => 'New Webhook', + 'url' => 'https://example.com/hook', + 'auth_method' => 'bearer_token', + 'subscribed_events' => ['ag.access_pass.issued'], + ]); + + $this->assertInstanceOf(Webhook::class, $webhook); + $this->assertEquals('wh_new123', $webhook->id); + $this->assertEquals('bearer_token', $webhook->authMethod); + $this->assertEquals('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', $webhook->privateKey); + $this->assertNull($webhook->clientCert); + } + + public function testCreateWebhookMtls(): void + { + $this->expectRequest('POST', '/v1/console/webhooks', 201, [ + 'id' => 'wh_mtls123', + 'name' => 'Secure Webhook', + 'url' => 'https://secure.example.com/hook', + 'auth_method' => 'mtls', + 'subscribed_events' => ['ag.access_pass.issued', 'ag.access_pass.deleted'], + 'created_at' => '2025-06-15T10:00:00Z', + 'client_cert' => '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----', + 'cert_expires_at' => '2025-12-15T10:00:00Z', + ]); + + $webhook = $this->client->console->createWebhook([ + 'name' => 'Secure Webhook', + 'url' => 'https://secure.example.com/hook', + 'auth_method' => 'mtls', + 'subscribed_events' => ['ag.access_pass.issued', 'ag.access_pass.deleted'], + ]); + + $this->assertInstanceOf(Webhook::class, $webhook); + $this->assertEquals('mtls', $webhook->authMethod); + $this->assertEquals('-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----', $webhook->clientCert); + $this->assertEquals('2025-12-15T10:00:00Z', $webhook->certExpiresAt); + $this->assertNull($webhook->privateKey); + } + + public function testDeleteWebhook(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('DELETE'), + $this->stringContains('/v1/console/webhooks/wh_abc123'), + $this->isType('array'), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(204, '')); + + $this->client->console->deleteWebhook('wh_abc123'); + } }