Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/AccessGridClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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]);
Expand All @@ -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 = [];
}
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
}
}
33 changes: 33 additions & 0 deletions src/Models/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace AccessGrid\Models;

use AccessGrid\AccessGridClient;

class Webhook
{
private AccessGridClient $client;
public ?string $id;
public ?string $name;
public ?string $url;
public ?string $authMethod;
public array $subscribedEvents;
public ?string $createdAt;
public ?string $privateKey;
public ?string $clientCert;
public ?string $certExpiresAt;

public function __construct(AccessGridClient $client, array $data)
{
$this->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;
}
}
35 changes: 35 additions & 0 deletions src/Services/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use AccessGrid\Models\Template;
use AccessGrid\Models\PassTemplatePair;
use AccessGrid\Models\LedgerItem;
use AccessGrid\Models\Webhook;

class Console
{
Expand Down Expand Up @@ -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
*/
Expand Down
82 changes: 82 additions & 0 deletions tests/Models/WebhookTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace AccessGrid\Tests\Models;

use AccessGrid\Tests\TestCase;
use AccessGrid\Models\Webhook;

class WebhookTest extends TestCase
{
public function testConstructWithBearerToken(): void
{
$webhook = new Webhook($this->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);
}
}
150 changes: 150 additions & 0 deletions tests/Services/ConsoleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use AccessGrid\Models\LedgerItem;
use AccessGrid\Models\LedgerItemAccessPass;
use AccessGrid\Models\LedgerItemPassTemplate;
use AccessGrid\Models\Webhook;

class ConsoleTest extends TestCase
{
Expand Down Expand Up @@ -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');
}
}
Loading