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..b87b80c 100644 --- a/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php +++ b/src/Adapter/Local/JSONFile/LocalJSONFileAdapter.php @@ -53,9 +53,14 @@ public function __construct(array $config) */ public function getSecret(string $key, ?array $options = []): Secret { - $secrets = $this->loadSecrets(); - $keys = array_column($secrets, 'key'); - $index = array_search($key, $keys, true); + try { + $secrets = $this->loadSecrets(); + } catch (\Exception $e) { + throw new SecretNotFoundException($key, $e); + } + + $keys = array_column($secrets, 'key'); + $index = array_search($key, $keys, true); if ($index === false || $index === null) { throw new SecretNotFoundException($key); @@ -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..39e0737 --- /dev/null +++ b/src/Adapter/Local/JSONFile/Tests/LocalJSONFileAdapterTest.php @@ -0,0 +1,155 @@ + + * @date 2019 + * @license https://opensource.org/licenses/MIT + */ + +namespace Secretary\Tests; + +use PHPUnit\Framework\TestCase; +use Secretary\Adapter\Local\JSONFile\LocalJSONFileAdapter; +use Secretary\Exception\SecretNotFoundException; +use Secretary\Secret; + +class LocalJSONFileAdapterTest extends TestCase +{ + private string $tempFile; + + protected function setUp(): void + { + parent::setUp(); + + $this->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..845e12e 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": { @@ -31,7 +31,7 @@ }, "autoload-dev": { "psr-4": { - "Secretary\\Adapter\\Local\\JSONFile\\Tests\\": "Tests/" + "Secretary\\Tests\\": "Tests/" } } } 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 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/" } } }