From c8b17632b8cb321a06275bef2530ddc8c5abb714 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Feb 2026 11:06:42 -0600 Subject: [PATCH 1/3] local adapter gracefully fails if file doesnt exist --- composer.json | 2 +- phpunit.xml.dist | 2 +- src/Adapter/Cache/PSR16Cache/.gitattributes | 1 + src/Adapter/Cache/PSR6Cache/.gitattributes | 1 + src/Adapter/Chain/.gitattributes | 1 + src/Adapter/GCP/SecretsManager/.gitattributes | 1 + src/Adapter/Hashicorp/Vault/.gitattributes | 1 + src/Adapter/Local/JSONFile/.gitattributes | 1 + .../Local/JSONFile/LocalJSONFileAdapter.php | 11 +- .../Tests/LocalJSONFileAdapterTest.php | 149 ++++++++++++++++++ src/Adapter/Local/JSONFile/composer.json | 2 +- src/Core/.gitattributes | 1 + 12 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/Adapter/Cache/PSR16Cache/.gitattributes create mode 100644 src/Adapter/Cache/PSR6Cache/.gitattributes create mode 100644 src/Adapter/Chain/.gitattributes create mode 100644 src/Adapter/GCP/SecretsManager/.gitattributes create mode 100644 src/Adapter/Hashicorp/Vault/.gitattributes create mode 100644 src/Adapter/Local/JSONFile/.gitattributes create mode 100644 src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php create mode 100644 src/Core/.gitattributes diff --git a/composer.json b/composer.json index 96d44de..ca1519a 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "google/cloud-secret-manager": "^2.2", "guzzlehttp/guzzle": "^7.0", "mockery/mockery": "^1.6.12", - "phpunit/phpunit": "^10.5 || ^11.0", + "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0 || ^13.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "symfony/config": "^5.3 || ^6.0 || ^7.0 || ^8.0", "symfony/dependency-injection": "^5.0 || ^6.0 || ^7.0 || ^8.0", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5165e63..ac16c78 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - + diff --git a/src/Adapter/Cache/PSR16Cache/.gitattributes b/src/Adapter/Cache/PSR16Cache/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/Cache/PSR16Cache/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/Cache/PSR6Cache/.gitattributes b/src/Adapter/Cache/PSR6Cache/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/Cache/PSR6Cache/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/Chain/.gitattributes b/src/Adapter/Chain/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/Chain/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/GCP/SecretsManager/.gitattributes b/src/Adapter/GCP/SecretsManager/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/GCP/SecretsManager/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/Hashicorp/Vault/.gitattributes b/src/Adapter/Hashicorp/Vault/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/Hashicorp/Vault/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/Local/JSONFile/.gitattributes b/src/Adapter/Local/JSONFile/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/Local/JSONFile/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php b/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php index 306d281..d345788 100644 --- a/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php +++ b/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php @@ -53,7 +53,12 @@ public function __construct(array $config) */ public function getSecret(string $key, ?array $options = []): Secret { - $secrets = $this->loadSecrets(); + try { + $secrets = $this->loadSecrets(); + } catch (\Exception $e) { + throw new SecretNotFoundException($key, $e); + } + $keys = array_column($secrets, 'key'); $index = array_search($key, $keys, true); @@ -130,6 +135,10 @@ private static function updateValue(Secret $secret, array $secrets): array */ private function loadSecrets(): array { + if (!file_exists($this->secretsFile)) { + throw new \Exception('Secrets file does not exist.'); + } + return json_decode(file_get_contents($this->secretsFile), true, 512, JSON_THROW_ON_ERROR); } diff --git a/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php new file mode 100644 index 0000000..280616d --- /dev/null +++ b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php @@ -0,0 +1,149 @@ +tempFile = tempnam(sys_get_temp_dir(), 'secretary_test_'); + file_put_contents($this->tempFile, json_encode([ + ['key' => 'db/password', 'value' => 's3cret'], + ['key' => 'api/token', 'value' => 'tok123', 'metadata' => ['env' => 'test']], + ])); + } + + protected function tearDown(): void + { + if (file_exists($this->tempFile)) { + unlink($this->tempFile); + } + + parent::tearDown(); + } + + public function testConstructThrowsWhenConfigIsEmpty(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Configuration is required.'); + + new LocalJSONFileAdapter([]); + } + + public function testConstructThrowsWhenFileMissing(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('`file` is a required config.'); + + new LocalJSONFileAdapter(['foo' => 'bar']); + } + + public function testGetSecretReturnsSecret(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + + $secret = $adapter->getSecret('db/password'); + + $this->assertInstanceOf(Secret::class, $secret); + $this->assertSame('db/password', $secret->getKey()); + $this->assertSame('s3cret', $secret->getValue()); + } + + public function testGetSecretReturnsMetadata(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + + $secret = $adapter->getSecret('api/token'); + + $this->assertSame(['env' => 'test'], $secret->getMetadata()); + } + + public function testGetSecretThrowsWhenKeyNotFound(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + + $this->expectException(SecretNotFoundException::class); + $this->expectExceptionMessage('No secret was found with the key: "nonexistent"'); + + $adapter->getSecret('nonexistent'); + } + + public function testGetSecretThrowsWhenFileDoesNotExist(): void + { + $adapter = new LocalJSONFileAdapter(['file' => '/tmp/secretary_nonexistent_file.json']); + + $this->expectException(SecretNotFoundException::class); + $this->expectExceptionMessage('No secret was found with the key: "any-key"'); + + try { + $adapter->getSecret('any-key'); + } catch (SecretNotFoundException $e) { + $this->assertNotNull($e->getPrevious(), 'Expected a previous exception to be set'); + $this->assertSame('Secrets file does not exist.', $e->getPrevious()->getMessage()); + + throw $e; + } + } + + public function testPutSecretAddsNewSecret(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + $secret = new Secret('new/key', 'new-value'); + + $result = $adapter->putSecret($secret); + + $this->assertSame('new/key', $result->getKey()); + $this->assertSame('new-value', $result->getValue()); + } + + public function testPutSecretUpdatesExistingSecret(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + $secret = new Secret('db/password', 'updated'); + + $adapter->putSecret($secret); + + $retrieved = $adapter->getSecret('db/password'); + $this->assertSame('updated', $retrieved->getValue()); + } + + public function testDeleteSecretByKey(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + + $adapter->deleteSecretByKey('db/password'); + + $this->expectException(SecretNotFoundException::class); + $adapter->getSecret('db/password'); + } + + public function testDeleteSecretByKeyThrowsWhenNotFound(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + + $this->expectException(SecretNotFoundException::class); + $adapter->deleteSecretByKey('nonexistent'); + } + + public function testDeleteSecret(): void + { + $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); + $secret = new Secret('api/token', 'tok123'); + + $adapter->deleteSecret($secret); + + $this->expectException(SecretNotFoundException::class); + $adapter->getSecret('api/token'); + } +} diff --git a/src/Adapter/Local/JSONFile/composer.json b/src/Adapter/Local/JSONFile/composer.json index 54da19c..c4ff808 100644 --- a/src/Adapter/Local/JSONFile/composer.json +++ b/src/Adapter/Local/JSONFile/composer.json @@ -22,7 +22,7 @@ }, "require-dev": { "mockery/mockery": "^1.6.12", - "phpunit/phpunit": "^10.5 || ^11.0" + "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0 || ^13.0" }, "autoload": { "psr-4": { diff --git a/src/Core/.gitattributes b/src/Core/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Core/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore From bd7682d1648a6d56f7318289645c58155f49afca Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Feb 2026 11:14:15 -0600 Subject: [PATCH 2/3] fixed tests --- src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php | 4 ++-- .../Local/JSONFile/Tests/LocalJSONFileAdapterTest.php | 8 ++++---- src/Adapter/Local/JSONFile/composer.json | 2 +- src/Core/composer.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php b/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php index d345788..b87b80c 100644 --- a/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php +++ b/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php @@ -59,8 +59,8 @@ public function getSecret(string $key, ?array $options = []): Secret throw new SecretNotFoundException($key, $e); } - $keys = array_column($secrets, 'key'); - $index = array_search($key, $keys, true); + $keys = array_column($secrets, 'key'); + $index = array_search($key, $keys, true); if ($index === false || $index === null) { throw new SecretNotFoundException($key); diff --git a/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php index 280616d..cc89e56 100644 --- a/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php +++ b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Secretary\Adapter\Local\JSONFile\Tests; +namespace Secretary\Tests; use PHPUnit\Framework\TestCase; use Secretary\Adapter\Local\JSONFile\LocalJSONFileAdapter; @@ -99,7 +99,7 @@ public function testGetSecretThrowsWhenFileDoesNotExist(): void public function testPutSecretAddsNewSecret(): void { $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); - $secret = new Secret('new/key', 'new-value'); + $secret = new Secret('new/key', 'new-value'); $result = $adapter->putSecret($secret); @@ -110,7 +110,7 @@ public function testPutSecretAddsNewSecret(): void public function testPutSecretUpdatesExistingSecret(): void { $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); - $secret = new Secret('db/password', 'updated'); + $secret = new Secret('db/password', 'updated'); $adapter->putSecret($secret); @@ -139,7 +139,7 @@ public function testDeleteSecretByKeyThrowsWhenNotFound(): void public function testDeleteSecret(): void { $adapter = new LocalJSONFileAdapter(['file' => $this->tempFile]); - $secret = new Secret('api/token', 'tok123'); + $secret = new Secret('api/token', 'tok123'); $adapter->deleteSecret($secret); diff --git a/src/Adapter/Local/JSONFile/composer.json b/src/Adapter/Local/JSONFile/composer.json index c4ff808..845e12e 100644 --- a/src/Adapter/Local/JSONFile/composer.json +++ b/src/Adapter/Local/JSONFile/composer.json @@ -31,7 +31,7 @@ }, "autoload-dev": { "psr-4": { - "Secretary\\Adapter\\Local\\JSONFile\\Tests\\": "Tests/" + "Secretary\\Tests\\": "Tests/" } } } diff --git a/src/Core/composer.json b/src/Core/composer.json index ab1a08c..12da30c 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -42,7 +42,7 @@ }, "autoload-dev": { "psr-4": { - "Secretary\\Tests\\": "tests/" + "Secretary\\Tests\\": "Tests/" } } } From c7e9e625b87717195c949916599b77b60c764e7e Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Feb 2026 11:15:38 -0600 Subject: [PATCH 3/3] added missing header --- .../Local/JSONFile/Tests/LocalJSONFileAdapterTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php index cc89e56..39e0737 100644 --- a/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php +++ b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php @@ -2,6 +2,12 @@ declare(strict_types=1); +/* + * @author Aaron Scherer + * @date 2019 + * @license https://opensource.org/licenses/MIT + */ + namespace Secretary\Tests; use PHPUnit\Framework\TestCase;