diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 0000000..4decf3b --- /dev/null +++ b/.envrc.example @@ -0,0 +1,3 @@ +# If using asdf + Homebrew on macOS, you might need these before installing PHP +# export PKG_CONFIG_PATH="$(brew --prefix openssl@3)/lib/pkgconfig:$PKG_CONFIG_PATH" +# export PHP_CONFIGURE_OPTIONS="--with-openssl=$(brew --prefix openssl@3)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d2f209d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + pull_request: + push: + branches: main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: ['7.4', '8.5'] + name: PHP ${{ matrix.php-version }} + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: curl, json, hash + coverage: none + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/composer-cache + key: php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: php-${{ matrix.php-version }}-composer- + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + env: + COMPOSER_CACHE_DIR: ${{ runner.temp }}/composer-cache + + - name: Run tests + run: composer test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b748f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Composer +/vendor/ +composer.lock + +# PHPUnit +.phpunit.result.cache +.phpunit.cache/ + +# PHP CS Fixer +# .php-cs-fixer.cache + +# OS files +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp + +# Environment +.env +.envrc diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..54ae07b --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +php 8.5.3 diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..575278d --- /dev/null +++ b/Brewfile @@ -0,0 +1,20 @@ +brew "autoconf" +brew "bison" +brew "curl" +brew "freetype" +brew "gd" +brew "gettext" +brew "gmp" +brew "icu4c@78" +brew "jpeg" +brew "libiconv" +brew "libpng" +brew "libsodium" +brew "libxml2" +brew "libzip" +brew "oniguruma" +brew "openssl@3" +brew "pkg-config" +brew "readline" +brew "re2c" +brew "webp" diff --git a/composer.json b/composer.json index dbdde74..ac38995 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "ext-hash": "*" }, "require-dev": { - "phpunit/phpunit": "^9.0|^10.0", + "phpunit/phpunit": "^9.0", "php-cs-fixer/shim": "^3.0" }, "autoload": { diff --git a/src/AccessGridClient.php b/src/AccessGridClient.php index 4d0a974..3bce409 100644 --- a/src/AccessGridClient.php +++ b/src/AccessGridClient.php @@ -4,6 +4,8 @@ use AccessGrid\Exceptions\AccessGridException; use AccessGrid\Exceptions\AuthenticationException; +use AccessGrid\Http\HttpClientInterface; +use AccessGrid\Http\CurlHttpClient; use AccessGrid\Services\AccessCards; use AccessGrid\Services\Console; @@ -12,15 +14,17 @@ class AccessGridClient private string $accountId; private string $secretKey; private string $baseUrl; + /** @var HttpClientInterface */ + private $httpClient; public AccessCards $accessCards; public Console $console; - public function __construct(string $accountId, string $secretKey, string $baseUrl = 'https://api.accessgrid.com') + public function __construct(string $accountId, string $secretKey, string $baseUrl = 'https://api.accessgrid.com', ?HttpClientInterface $httpClient = null) { if (empty($accountId)) { throw new \InvalidArgumentException('Account ID is required'); } - + if (empty($secretKey)) { throw new \InvalidArgumentException('Secret Key is required'); } @@ -28,7 +32,8 @@ public function __construct(string $accountId, string $secretKey, string $baseUr $this->accountId = $accountId; $this->secretKey = $secretKey; $this->baseUrl = rtrim($baseUrl, '/'); - + $this->httpClient = $httpClient ?? new CurlHttpClient(); + $this->accessCards = new AccessCards($this); $this->console = new Console($this); } @@ -110,18 +115,6 @@ public function makeRequest(string $method, string $endpoint, ?array $data = nul 'User-Agent: accessgrid-php @ v1.0.0' ]; - // Initialize cURL - $ch = curl_init(); - - // Set basic cURL options - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_TIMEOUT => 30, - CURLOPT_CUSTOMREQUEST => $method - ]); - // 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))) { @@ -130,43 +123,40 @@ public function makeRequest(string $method, string $endpoint, ?array $data = nul } // Include the ID payload in the query params if ($resourceId) { - // The server expects the raw JSON string, not URL-encoded $params['sig_payload'] = json_encode(['id' => $resourceId]); } } - - // Add query parameters for GET requests or when params are provided + + // Build final URL with query parameters + $finalUrl = $url; if (!empty($params)) { $queryString = http_build_query($params); $separator = strpos($url, '?') !== false ? '&' : '?'; - curl_setopt($ch, CURLOPT_URL, $url . $separator . $queryString); + $finalUrl = $url . $separator . $queryString; } - - // For POST/PUT/PATCH with data, set the JSON body + + // Build request body for POST/PUT/PATCH + $requestBody = null; if (!empty($data) && $method !== 'GET') { - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); - } - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - curl_close($ch); - - if ($error) { - throw new AccessGridException('Request failed: ' . $error); + $requestBody = json_encode($data); } - + + // Delegate to HTTP client + $response = $this->httpClient->send($method, $finalUrl, $headers, $requestBody); + $httpCode = $response->getStatusCode(); + $responseBody = $response->getBody(); + if ($httpCode === 401) { throw new AuthenticationException('Invalid credentials'); } elseif ($httpCode === 402) { throw new AccessGridException('Insufficient account balance'); } elseif ($httpCode < 200 || $httpCode >= 300) { - $errorData = json_decode($response, true) ?: []; - $errorMessage = $errorData['message'] ?? $response; + $errorData = json_decode($responseBody, true) ?: []; + $errorMessage = $errorData['message'] ?? $responseBody; throw new AccessGridException('API request failed: ' . $errorMessage); } - $decoded = json_decode($response, true); + $decoded = json_decode($responseBody, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new AccessGridException('Invalid JSON response: ' . json_last_error_msg()); } diff --git a/src/Http/CurlHttpClient.php b/src/Http/CurlHttpClient.php new file mode 100644 index 0000000..4a329db --- /dev/null +++ b/src/Http/CurlHttpClient.php @@ -0,0 +1,36 @@ + $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => 30, + CURLOPT_CUSTOMREQUEST => $method, + ]); + + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $responseBody = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + throw new AccessGridException('Request failed: ' . $error); + } + + return new HttpResponse($httpCode, $responseBody); + } +} diff --git a/src/Http/HttpClientInterface.php b/src/Http/HttpClientInterface.php new file mode 100644 index 0000000..5f432de --- /dev/null +++ b/src/Http/HttpClientInterface.php @@ -0,0 +1,18 @@ +statusCode = $statusCode; + $this->body = $body; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getBody(): string + { + return $this->body; + } +} diff --git a/tests/AccessGridClientTest.php b/tests/AccessGridClientTest.php index 4690e10..f839bc0 100644 --- a/tests/AccessGridClientTest.php +++ b/tests/AccessGridClientTest.php @@ -2,19 +2,12 @@ namespace AccessGrid\Tests; -use PHPUnit\Framework\TestCase; use AccessGrid\AccessGridClient; use AccessGrid\Exceptions\AccessGridException; use AccessGrid\Exceptions\AuthenticationException; class AccessGridClientTest extends TestCase { - private AccessGridClient $client; - - protected function setUp(): void - { - $this->client = new AccessGridClient('test-account-id', 'test-secret-key'); - } public function testConstructorThrowsExceptionForEmptyAccountId(): void { @@ -76,4 +69,112 @@ public function testGetConsole(): void $console = $this->client->getConsole(); $this->assertInstanceOf(\AccessGrid\Services\Console::class, $console); } + + public function testMakeRequestReturnsDecodedJson(): void + { + $this->mockResponse(200, ['id' => 'card_123', 'state' => 'active']); + + $result = $this->client->get('/v1/key-cards/card_123'); + + $this->assertEquals('card_123', $result['id']); + $this->assertEquals('active', $result['state']); + } + + public function testMakeRequestThrowsAuthenticationExceptionOn401(): void + { + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('Invalid credentials'); + + $this->mockResponse(401, ['message' => 'Unauthorized']); + + $this->client->get('/v1/key-cards/card_123'); + } + + public function testMakeRequestThrowsAccessGridExceptionOn402(): void + { + $this->expectException(AccessGridException::class); + $this->expectExceptionMessage('Insufficient account balance'); + + $this->mockResponse(402, ['message' => 'Insufficient account balance']); + + $this->client->get('/v1/key-cards/card_123'); + } + + public function testMakeRequestThrowsAccessGridExceptionOn500(): void + { + $this->expectException(AccessGridException::class); + $this->expectExceptionMessage('API request failed'); + + $this->mockResponse(500, ['message' => 'Internal server error']); + + $this->client->get('/v1/key-cards/card_123'); + } + + public function testMakeRequestThrowsAccessGridExceptionOnInvalidJson(): void + { + $this->expectException(AccessGridException::class); + $this->expectExceptionMessage('Invalid JSON response'); + + $this->mockHttpClient + ->method('send') + ->willReturn(new \AccessGrid\Http\HttpResponse(200, 'not valid json')); + + $this->client->get('/v1/key-cards/card_123'); + } + + public function testMakeRequestIncludesCorrectHeaders(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->anything(), + $this->anything(), + $this->callback(function (array $headers) { + $headerString = implode("\n", $headers); + return strpos($headerString, 'X-ACCT-ID: test-account-id') !== false + && strpos($headerString, 'X-PAYLOAD-SIG: ') !== false + && strpos($headerString, 'Content-Type: application/json') !== false + && strpos($headerString, 'User-Agent: accessgrid-php') !== false; + }), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, '{"ok":true}')); + + $this->client->get('/v1/key-cards/card_123'); + } + + public function testPostSendsJsonBody(): void + { + $data = ['full_name' => 'John Doe', 'card_template_id' => 'tmpl_1']; + + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('POST'), + $this->anything(), + $this->anything(), + $this->equalTo(json_encode($data)) + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, '{"id":"card_123"}')); + + $this->client->post('/v1/key-cards', $data); + } + + public function testGetAppendsQueryParams(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('GET'), + $this->stringContains('template_id=tmpl_1'), + $this->anything(), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, '{"keys":[]}')); + + $this->client->get('/v1/key-cards', ['template_id' => 'tmpl_1']); + } } \ No newline at end of file diff --git a/tests/Models/AccessCardTest.php b/tests/Models/AccessCardTest.php new file mode 100644 index 0000000..fe7cbbc --- /dev/null +++ b/tests/Models/AccessCardTest.php @@ -0,0 +1,106 @@ + 'card_123', + 'install_url' => 'https://example.com/install', + 'state' => 'active', + 'full_name' => 'John Doe', + 'expiration_date' => '2026-12-31', + 'card_number' => '12345', + 'site_code' => 'SC01', + 'file_data' => 'base64data', + 'direct_install_url' => 'https://example.com/direct', + 'details' => ['foo' => 'bar'], + 'devices' => [['device_id' => 'dev_1']], + 'metadata' => ['key' => 'value'], + ]; + + $card = new AccessCard($this->client, $data); + + $this->assertEquals('card_123', $card->id); + $this->assertEquals('https://example.com/install', $card->install_url); + $this->assertEquals('https://example.com/install', $card->url); + $this->assertEquals('active', $card->state); + $this->assertEquals('John Doe', $card->full_name); + $this->assertEquals('2026-12-31', $card->expiration_date); + $this->assertEquals('12345', $card->card_number); + $this->assertEquals('SC01', $card->site_code); + $this->assertEquals('base64data', $card->file_data); + $this->assertEquals('https://example.com/direct', $card->direct_install_url); + $this->assertEquals(['foo' => 'bar'], $card->details); + $this->assertCount(1, $card->devices); + $this->assertEquals('value', $card->metadata['key']); + } + + public function testCamelCaseAndSnakeCaseProperties(): void + { + $data = [ + 'full_name' => 'Jane Doe', + 'expiration_date' => '2027-01-01', + 'card_number' => '67890', + 'site_code' => 'SC02', + 'file_data' => 'somedata', + 'direct_install_url' => 'https://example.com/direct2', + ]; + + $card = new AccessCard($this->client, $data); + + $this->assertEquals($card->full_name, $card->fullName); + $this->assertEquals($card->expiration_date, $card->expirationDate); + $this->assertEquals($card->card_number, $card->cardNumber); + $this->assertEquals($card->site_code, $card->siteCode); + $this->assertEquals($card->file_data, $card->fileData); + $this->assertEquals($card->direct_install_url, $card->directInstallUrl); + } + + public function testConstructionWithMinimalData(): void + { + $card = new AccessCard($this->client, ['id' => 'card_456']); + + $this->assertEquals('card_456', $card->id); + $this->assertNull($card->state); + $this->assertNull($card->full_name); + $this->assertNull($card->install_url); + $this->assertNull($card->expiration_date); + $this->assertNull($card->card_number); + $this->assertNull($card->site_code); + $this->assertNull($card->file_data); + $this->assertNull($card->direct_install_url); + $this->assertNull($card->details); + } + + public function testDefaultDevicesAndMetadata(): void + { + $card = new AccessCard($this->client, []); + + $this->assertEquals([], $card->devices); + $this->assertEquals([], $card->metadata); + } + + public function testToString(): void + { + $card = new AccessCard($this->client, [ + 'id' => 'card_123', + 'full_name' => 'John Doe', + 'state' => 'active', + ]); + + $this->assertEquals("AccessCard(name='John Doe', id='card_123', state='active')", (string) $card); + } + + public function testToStringWithNulls(): void + { + $card = new AccessCard($this->client, []); + + $this->assertEquals("AccessCard(name='', id='', state='')", (string) $card); + } +} diff --git a/tests/Models/TemplateTest.php b/tests/Models/TemplateTest.php new file mode 100644 index 0000000..28b869a --- /dev/null +++ b/tests/Models/TemplateTest.php @@ -0,0 +1,72 @@ + 'tmpl_123', + 'name' => 'Employee Badge', + 'platform' => 'apple', + 'use_case' => 'employee_badge', + 'protocol' => 'desfire', + 'created_at' => '2025-01-01T00:00:00Z', + 'last_published_at' => '2025-06-01T00:00:00Z', + 'issued_keys_count' => 100, + 'active_keys_count' => 85, + 'allowed_device_counts' => ['iphone' => 3, 'watch' => 1], + 'support_settings' => ['support_email' => 'help@example.com'], + 'terms_settings' => ['privacy_policy_url' => 'https://example.com/privacy'], + 'style_settings' => ['background_color' => '#FFFFFF'], + ]; + + $template = new Template($this->client, $data); + + $this->assertEquals('tmpl_123', $template->id); + $this->assertEquals('Employee Badge', $template->name); + $this->assertEquals('apple', $template->platform); + $this->assertEquals('employee_badge', $template->useCase); + $this->assertEquals('desfire', $template->protocol); + $this->assertEquals('2025-01-01T00:00:00Z', $template->createdAt); + $this->assertEquals('2025-06-01T00:00:00Z', $template->lastPublishedAt); + $this->assertEquals(100, $template->issuedKeysCount); + $this->assertEquals(85, $template->activeKeysCount); + $this->assertEquals(['iphone' => 3, 'watch' => 1], $template->allowedDeviceCounts); + $this->assertEquals('help@example.com', $template->supportSettings['support_email']); + $this->assertEquals('https://example.com/privacy', $template->termsSettings['privacy_policy_url']); + $this->assertEquals('#FFFFFF', $template->styleSettings['background_color']); + } + + public function testConstructionWithMinimalData(): void + { + $template = new Template($this->client, [ + 'id' => 'tmpl_456', + 'name' => 'Basic Template', + ]); + + $this->assertEquals('tmpl_456', $template->id); + $this->assertEquals('Basic Template', $template->name); + $this->assertNull($template->platform); + $this->assertNull($template->useCase); + $this->assertNull($template->protocol); + $this->assertNull($template->createdAt); + $this->assertNull($template->lastPublishedAt); + $this->assertNull($template->issuedKeysCount); + $this->assertNull($template->activeKeysCount); + } + + public function testNullableArrayProperties(): void + { + $template = new Template($this->client, []); + + $this->assertNull($template->allowedDeviceCounts); + $this->assertNull($template->supportSettings); + $this->assertNull($template->termsSettings); + $this->assertNull($template->styleSettings); + } +} diff --git a/tests/Services/AccessCardsTest.php b/tests/Services/AccessCardsTest.php new file mode 100644 index 0000000..e655160 --- /dev/null +++ b/tests/Services/AccessCardsTest.php @@ -0,0 +1,188 @@ +expectRequest('POST', '/v1/key-cards', 200, [ + 'id' => 'card_123', + 'state' => 'active', + 'install_url' => 'https://example.com/install', + ]); + + $card = $this->client->accessCards->issue([ + 'card_template_id' => 'tmpl_1', + 'full_name' => 'John Doe', + ]); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('card_123', $card->id); + $this->assertEquals('active', $card->state); + $this->assertEquals('https://example.com/install', $card->install_url); + } + + public function testProvisionDelegatesToIssue(): void + { + $this->expectRequest('POST', '/v1/key-cards', 200, [ + 'id' => 'card_456', + 'state' => 'active', + ]); + + $card = $this->client->accessCards->provision([ + 'card_template_id' => 'tmpl_1', + 'full_name' => 'Jane Doe', + ]); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('card_456', $card->id); + } + + public function testGet(): void + { + $this->expectRequest('GET', '/v1/key-cards/card_123', 200, [ + 'id' => 'card_123', + 'state' => 'active', + 'full_name' => 'John Doe', + 'expiration_date' => '2026-12-31', + 'card_number' => '12345', + 'site_code' => 'SC01', + 'devices' => [['device_id' => 'dev_1']], + 'metadata' => ['key' => 'value'], + ]); + + $card = $this->client->accessCards->get('card_123'); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('card_123', $card->id); + $this->assertEquals('John Doe', $card->full_name); + $this->assertEquals('2026-12-31', $card->expiration_date); + $this->assertEquals('12345', $card->card_number); + $this->assertEquals('SC01', $card->site_code); + $this->assertCount(1, $card->devices); + $this->assertEquals('value', $card->metadata['key']); + } + + public function testUpdate(): void + { + $this->expectRequest('PATCH', '/v1/key-cards/card_123', 200, [ + 'id' => 'card_123', + 'full_name' => 'Jane Updated', + 'state' => 'active', + ]); + + $card = $this->client->accessCards->update([ + 'card_id' => 'card_123', + 'full_name' => 'Jane Updated', + ]); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('Jane Updated', $card->full_name); + } + + public function testList(): void + { + $this->expectRequest('GET', '/v1/key-cards', 200, [ + 'keys' => [ + ['id' => 'card_1', 'state' => 'active'], + ['id' => 'card_2', 'state' => 'active'], + ], + ]); + + $cards = $this->client->accessCards->list('tmpl_1'); + + $this->assertCount(2, $cards); + $this->assertInstanceOf(AccessCard::class, $cards[0]); + $this->assertEquals('card_1', $cards[0]->id); + $this->assertEquals('card_2', $cards[1]->id); + } + + public function testListWithStateFilter(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('GET'), + $this->callback(function (string $url) { + return strpos($url, 'template_id=tmpl_1') !== false + && strpos($url, 'state=suspended') !== false; + }), + $this->anything(), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, json_encode([ + 'keys' => [['id' => 'card_3', 'state' => 'suspended']], + ]))); + + $cards = $this->client->accessCards->list('tmpl_1', 'suspended'); + + $this->assertCount(1, $cards); + $this->assertEquals('suspended', $cards[0]->state); + } + + public function testListEmptyResponse(): void + { + $this->mockResponse(200, ['keys' => []]); + + $cards = $this->client->accessCards->list('tmpl_1'); + + $this->assertCount(0, $cards); + } + + public function testSuspend(): void + { + $this->expectRequest('POST', '/v1/key-cards/card_123/suspend', 200, [ + 'id' => 'card_123', + 'state' => 'suspended', + ]); + + $card = $this->client->accessCards->suspend(['card_id' => 'card_123']); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('suspended', $card->state); + } + + public function testResume(): void + { + $this->expectRequest('POST', '/v1/key-cards/card_123/resume', 200, [ + 'id' => 'card_123', + 'state' => 'active', + ]); + + $card = $this->client->accessCards->resume(['card_id' => 'card_123']); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('active', $card->state); + } + + public function testUnlink(): void + { + $this->expectRequest('POST', '/v1/key-cards/card_123/unlink', 200, [ + 'id' => 'card_123', + 'state' => 'unlinked', + ]); + + $card = $this->client->accessCards->unlink(['card_id' => 'card_123']); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('unlinked', $card->state); + } + + public function testDelete(): void + { + $this->expectRequest('POST', '/v1/key-cards/card_123/delete', 200, [ + 'id' => 'card_123', + 'state' => 'deleted', + ]); + + $card = $this->client->accessCards->delete(['card_id' => 'card_123']); + + $this->assertInstanceOf(AccessCard::class, $card); + $this->assertEquals('deleted', $card->state); + } +} diff --git a/tests/Services/ConsoleTest.php b/tests/Services/ConsoleTest.php new file mode 100644 index 0000000..714dcab --- /dev/null +++ b/tests/Services/ConsoleTest.php @@ -0,0 +1,184 @@ +expectRequest('POST', '/v1/console/card-templates', 200, [ + 'id' => 'tmpl_123', + 'name' => 'Employee Badge', + 'platform' => 'apple', + 'protocol' => 'desfire', + ]); + + $template = $this->client->console->createTemplate([ + 'name' => 'Employee Badge', + 'platform' => 'apple', + 'use_case' => 'employee_badge', + 'protocol' => 'desfire', + ]); + + $this->assertInstanceOf(Template::class, $template); + $this->assertEquals('tmpl_123', $template->id); + $this->assertEquals('Employee Badge', $template->name); + $this->assertEquals('apple', $template->platform); + } + + public function testUpdateTemplate(): void + { + $this->expectRequest('PUT', '/v1/console/card-templates/tmpl_123', 200, [ + 'id' => 'tmpl_123', + 'name' => 'Updated Badge', + ]); + + $template = $this->client->console->updateTemplate([ + 'card_template_id' => 'tmpl_123', + 'name' => 'Updated Badge', + ]); + + $this->assertInstanceOf(Template::class, $template); + $this->assertEquals('Updated Badge', $template->name); + } + + public function testReadTemplate(): void + { + $this->expectRequest('GET', '/v1/console/card-templates/tmpl_123', 200, [ + 'id' => 'tmpl_123', + 'name' => 'Employee Badge', + 'platform' => 'apple', + 'protocol' => 'desfire', + 'use_case' => 'employee_badge', + 'created_at' => '2025-01-01T00:00:00Z', + 'last_published_at' => '2025-06-01T00:00:00Z', + 'issued_keys_count' => 100, + 'active_keys_count' => 85, + 'allowed_device_counts' => ['iphone' => 3, 'watch' => 1], + 'support_settings' => ['support_email' => 'help@example.com'], + 'terms_settings' => ['privacy_policy_url' => 'https://example.com/privacy'], + 'style_settings' => ['background_color' => '#FFFFFF'], + ]); + + $template = $this->client->console->readTemplate([ + 'card_template_id' => 'tmpl_123', + ]); + + $this->assertInstanceOf(Template::class, $template); + $this->assertEquals('tmpl_123', $template->id); + $this->assertEquals('employee_badge', $template->useCase); + $this->assertEquals(100, $template->issuedKeysCount); + $this->assertEquals(85, $template->activeKeysCount); + $this->assertEquals('#FFFFFF', $template->styleSettings['background_color']); + } + + public function testGetLogs(): void + { + $this->expectRequest('GET', '/v1/console/card-templates/tmpl_123/logs', 200, [ + 'logs' => [ + ['event' => 'access_pass.device_added', 'created_at' => '2025-06-01T12:00:00Z'], + ['event' => 'access_pass.device_removed', 'created_at' => '2025-06-02T12:00:00Z'], + ], + 'pagination' => ['current_page' => 1, 'total_pages' => 3], + ]); + + $result = $this->client->console->getLogs('tmpl_123'); + + $this->assertArrayHasKey('logs', $result); + $this->assertCount(2, $result['logs']); + $this->assertEquals('access_pass.device_added', $result['logs'][0]['event']); + } + + public function testGetLogsWithParams(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('GET'), + $this->callback(function (string $url) { + return strpos($url, '/v1/console/card-templates/tmpl_123/logs') !== false + && strpos($url, 'device=mobile') !== false; + }), + $this->anything(), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, json_encode([ + 'logs' => [], + 'pagination' => ['current_page' => 1, 'total_pages' => 1], + ]))); + + $result = $this->client->console->getLogs('tmpl_123', ['device' => 'mobile']); + + $this->assertArrayHasKey('logs', $result); + } + + public function testEventLog(): void + { + $this->mockResponse(200, [ + 'events' => [ + ['event' => 'access_pass.device_added', 'created_at' => '2025-06-01T12:00:00Z', 'ip_address' => '1.2.3.4'], + ['event' => 'access_pass.suspended', 'created_at' => '2025-06-02T12:00:00Z', 'ip_address' => '5.6.7.8'], + ], + ]); + + $events = $this->client->console->eventLog([ + 'card_template_id' => 'tmpl_123', + ]); + + $this->assertCount(2, $events); + $this->assertIsObject($events[0]); + $this->assertEquals('access_pass.device_added', $events[0]->event); + $this->assertEquals('1.2.3.4', $events[0]->ip_address); + } + + public function testEventLogWithFilters(): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('GET'), + $this->callback(function (string $url) { + return strpos($url, 'event_type=access_pass.device_added') !== false; + }), + $this->anything(), + $this->anything() + ) + ->willReturn(new \AccessGrid\Http\HttpResponse(200, json_encode([ + 'events' => [ + ['event' => 'access_pass.device_added', 'created_at' => '2025-06-01T12:00:00Z'], + ], + ]))); + + $events = $this->client->console->eventLog([ + 'card_template_id' => 'tmpl_123', + 'filters' => ['event_type' => 'access_pass.device_added'], + ]); + + $this->assertCount(1, $events); + } + + public function testIosPreflight(): void + { + $this->expectRequest('POST', '/v1/console/ios-preflight', 200, [ + 'provisioningCredentialIdentifier' => 'prov_123', + 'sharingInstanceIdentifier' => 'share_456', + 'cardTemplateIdentifier' => 'tmpl_123', + 'environmentIdentifier' => 'production', + ]); + + $result = $this->client->console->iosPreflight([ + 'card_template_id' => 'tmpl_123', + 'access_pass_ex_id' => 'pass_789', + ]); + + $this->assertIsObject($result); + $this->assertEquals('prov_123', $result->provisioningCredentialIdentifier); + $this->assertEquals('share_456', $result->sharingInstanceIdentifier); + $this->assertEquals('production', $result->environmentIdentifier); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..60833bb --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,55 @@ +mockHttpClient = $this->createMock(HttpClientInterface::class); + $this->client = new AccessGridClient( + 'test-account-id', + 'test-secret-key', + 'https://api.accessgrid.com', + $this->mockHttpClient + ); + } + + /** + * Configure the mock to return a response on the next send() call. + */ + protected function mockResponse(int $status, array $body): void + { + $this->mockHttpClient + ->method('send') + ->willReturn(new HttpResponse($status, json_encode($body))); + } + + /** + * Configure the mock to return a response and assert what was sent. + */ + protected function expectRequest(string $method, string $urlContains, int $responseStatus, array $responseBody): void + { + $this->mockHttpClient + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo($method), + $this->stringContains($urlContains), + $this->isType('array'), + $this->anything() + ) + ->willReturn(new HttpResponse($responseStatus, json_encode($responseBody))); + } +}