diff --git a/composer.json b/composer.json index ca1519a..1700f14 100644 --- a/composer.json +++ b/composer.json @@ -1,51 +1,51 @@ { - "name": "secretary/php", - "description": "Monorepo for Secretary's PHP implementation", - "type": "library", - "require-dev": { - "php": "^8.2", - "ext-json": "*", - "aws/aws-sdk-php": "^3.91", - "google/cloud-secret-manager": "^2.2", - "guzzlehttp/guzzle": "^7.0", - "mockery/mockery": "^1.6.12", - "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", - "symfony/framework-bundle": "^5.0 || ^6.0 || ^7.0 || ^8.0", - "symfony/http-kernel": "^5.0 || ^6.0 || ^7.0 || ^8.0", - "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/yaml": "^5.0 || ^6.0 || ^7.0 || ^8.0", - "symplify/easy-coding-standard": "^12", - "vimeo/psalm": "^5.26 || ^6.14.3" - }, - "license": "MIT", - "authors": [ - { - "name": "Aaron Scherer", - "email": "aequasi@gmail.com" - } - ], - "autoload": { - "psr-4": { - "Secretary\\": "src/Core", - "Secretary\\Adapter\\": "src/Adapter", - "Secretary\\Bundle\\": "src/Bundle" - }, - "exclude-from-classmap": [ - "**/Tests/" - ] - }, - "config": { - "preferred-install": { - "*": "dist" - }, - "sort-packages": true - }, - "scripts": { - "ecs": "ecs check", - "ecs:fix": "ecs check --fix", - "psalm": "psalm --show-info" - } + "name": "secretary/php", + "description": "Monorepo for Secretary's PHP implementation", + "type": "library", + "require-dev": { + "php": "^8.2", + "ext-json": "*", + "aws/aws-sdk-php": "^3.91", + "google/cloud-secret-manager": "^2.2", + "guzzlehttp/guzzle": "^7.0", + "mockery/mockery": "^1.6.12", + "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", + "symfony/framework-bundle": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-kernel": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symplify/easy-coding-standard": "^12", + "vimeo/psalm": "^5.26 || ^6.14.3" + }, + "license": "MIT", + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com" + } + ], + "autoload": { + "psr-4": { + "Secretary\\": "src/Core", + "Secretary\\Adapter\\": "src/Adapter", + "Secretary\\Bundle\\": "src/Bundle" + }, + "exclude-from-classmap": [ + "**/Tests/" + ] + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "scripts": { + "ecs": "ecs check", + "ecs:fix": "ecs check --fix", + "psalm": "psalm --show-info" + } } diff --git a/psalm.xml b/psalm.xml index 11ea74c..49f6001 100644 --- a/psalm.xml +++ b/psalm.xml @@ -11,8 +11,8 @@ - - + + diff --git a/src/Adapter/AWS/SecretsManager/.gitattributes b/src/Adapter/AWS/SecretsManager/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Adapter/AWS/SecretsManager/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Adapter/AWS/SecretsManager/Tests/AWSSecretsManagerAdapterTest.php b/src/Adapter/AWS/SecretsManager/Tests/AWSSecretsManagerAdapterTest.php new file mode 100644 index 0000000..0664082 --- /dev/null +++ b/src/Adapter/AWS/SecretsManager/Tests/AWSSecretsManagerAdapterTest.php @@ -0,0 +1,202 @@ + + * @date 2019 + * @license https://opensource.org/licenses/MIT + */ + +namespace Secretary\Tests; + +use Aws\CommandInterface; +use Aws\Result; +use Aws\SecretsManager\Exception\SecretsManagerException; +use Aws\SecretsManager\SecretsManagerClient; +use Mockery\MockInterface; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; +use Secretary\Adapter\AWS\SecretsManager\AWSSecretsManagerAdapter; +use Secretary\Exception\SecretNotFoundException; +use Secretary\Secret; + +#[CoversClass(AWSSecretsManagerAdapter::class)] +class AWSSecretsManagerAdapterTest extends TestCase +{ + private AWSSecretsManagerAdapter $adapter; + + private SecretsManagerClient|MockInterface $client; + + protected function setUp(): void + { + parent::setUp(); + + $this->client = \Mockery::mock(SecretsManagerClient::class); + $this->adapter = new AWSSecretsManagerAdapter([]); + + $reflection = new \ReflectionProperty(AWSSecretsManagerAdapter::class, 'client'); + $reflection->setValue($this->adapter, $this->client); + } + + protected function tearDown(): void + { + \Mockery::close(); + parent::tearDown(); + } + + public function testGetSecretWithStringValue(): void + { + $result = new Result(['SecretString' => 'my-secret-value']); + + $this->client + ->shouldReceive('getSecretValue') + ->with(\Mockery::on(fn (array $opts) => $opts['SecretId'] === 'my/key')) + ->once() + ->andReturn($result); + + $secret = $this->adapter->getSecret('my/key'); + + $this->assertInstanceOf(Secret::class, $secret); + $this->assertEquals('my/key', $secret->getKey()); + $this->assertEquals('my-secret-value', $secret->getValue()); + } + + public function testGetSecretWithJsonValue(): void + { + $jsonData = ['username' => 'admin', 'password' => 'secret123']; + $result = new Result(['SecretString' => json_encode($jsonData)]); + + $this->client + ->shouldReceive('getSecretValue') + ->with(\Mockery::on(fn (array $opts) => $opts['SecretId'] === 'db/credentials')) + ->once() + ->andReturn($result); + + $secret = $this->adapter->getSecret('db/credentials'); + + $this->assertInstanceOf(Secret::class, $secret); + $this->assertEquals('db/credentials', $secret->getKey()); + $this->assertEquals($jsonData, $secret->getValue()); + } + + public function testGetSecretThrowsSecretNotFoundException(): void + { + $this->expectException(SecretNotFoundException::class); + + $command = \Mockery::mock(CommandInterface::class); + $exception = new SecretsManagerException( + 'Error', + $command, + ['message' => "Secrets Manager can\u{2019}t find the specified secret"] + ); + + $this->client + ->shouldReceive('getSecretValue') + ->once() + ->andThrow($exception); + + $this->adapter->getSecret('nonexistent/key'); + } + + public function testGetSecretRethrowsOtherExceptions(): void + { + $this->expectException(SecretsManagerException::class); + + $command = \Mockery::mock(CommandInterface::class); + $exception = new SecretsManagerException( + 'Access denied', + $command, + ['message' => 'User is not authorized'] + ); + + $this->client + ->shouldReceive('getSecretValue') + ->once() + ->andThrow($exception); + + $this->adapter->getSecret('forbidden/key'); + } + + public function testPutSecretUpdatesExisting(): void + { + $secret = new Secret('my/key', 'my-value'); + + $this->client + ->shouldReceive('updateSecret') + ->with(\Mockery::on(function (array $opts) { + return $opts['SecretId'] === 'my/key' + && $opts['SecretString'] === 'my-value'; + })) + ->once(); + + $result = $this->adapter->putSecret($secret); + + $this->assertSame($secret, $result); + } + + public function testPutSecretCreatesWhenUpdateFails(): void + { + $secret = new Secret('new/key', 'new-value'); + + $this->client + ->shouldReceive('updateSecret') + ->once() + ->andThrow(new \Exception('Secret not found')); + + $this->client + ->shouldReceive('createSecret') + ->with(\Mockery::on(function (array $opts) { + return $opts['Name'] === 'new/key' + && $opts['SecretString'] === 'new-value'; + })) + ->once(); + + $result = $this->adapter->putSecret($secret); + + $this->assertSame($secret, $result); + } + + public function testPutSecretWithArrayValue(): void + { + $value = ['user' => 'admin', 'pass' => 'secret']; + $secret = new Secret('my/key', $value); + + $this->client + ->shouldReceive('updateSecret') + ->with(\Mockery::on(function (array $opts) use ($value) { + return $opts['SecretString'] === json_encode($value); + })) + ->once(); + + $result = $this->adapter->putSecret($secret); + + $this->assertSame($secret, $result); + } + + public function testDeleteSecretByKey(): void + { + $this->client + ->shouldReceive('deleteSecret') + ->with(\Mockery::on(fn (array $opts) => $opts['SecretId'] === 'my/key')) + ->once(); + + $this->adapter->deleteSecretByKey('my/key'); + + $this->assertTrue(true); + } + + public function testDeleteSecret(): void + { + $secret = new Secret('my/key', 'value'); + + $this->client + ->shouldReceive('deleteSecret') + ->with(\Mockery::on(fn (array $opts) => $opts['SecretId'] === 'my/key')) + ->once(); + + $this->adapter->deleteSecret($secret); + + $this->assertTrue(true); + } +} diff --git a/src/Adapter/AWS/SecretsManager/composer.json b/src/Adapter/AWS/SecretsManager/composer.json index c772188..21659fc 100644 --- a/src/Adapter/AWS/SecretsManager/composer.json +++ b/src/Adapter/AWS/SecretsManager/composer.json @@ -1,39 +1,33 @@ { - "name": "secretary/aws-secrets-manager-adapter", - "description": "AWS Secrets Manager Adapter for Secretary", - "type": "library", - "license": "MIT", - "keywords": [ + "name": "secretary/aws-secrets-manager-adapter", + "description": "AWS Secrets Manager Adapter for Secretary", + "type": "library", + "license": "MIT", + "keywords": [ "secrets", "aws", "aws secrets manager", "secretary" ], - "authors": [ + "authors": [ { - "name": "Aaron Scherer", + "name": "Aaron Scherer", "email": "aequasi@gmail.com" } ], "minimum-stability": "stable", - "require": { + "require": { "php": "^8.2", - "ext-json": "*", + "ext-json": "*", "aws/aws-sdk-php": "^3.0", - "secretary/core": "self.version" + "secretary/core": "self.version" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, - "autoload": { + "autoload": { "psr-4": { "Secretary\\Adapter\\AWS\\SecretsManager\\": "" - } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Adapter\\AWS\\SecretsManager\\Tests\\": "Tests/" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] } } diff --git a/src/Adapter/Cache/PSR16Cache/composer.json b/src/Adapter/Cache/PSR16Cache/composer.json index dbd05ed..4b0eff4 100644 --- a/src/Adapter/Cache/PSR16Cache/composer.json +++ b/src/Adapter/Cache/PSR16Cache/composer.json @@ -21,18 +21,9 @@ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "secretary/core": "self.version" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, "autoload": { "psr-4": { "Secretary\\Adapter\\Cache\\PSR16Cache\\": "" } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Adapter\\Cache\\PSR16Cache\\Tests\\": "tests/" - } } } diff --git a/src/Adapter/Cache/PSR6Cache/composer.json b/src/Adapter/Cache/PSR6Cache/composer.json index 6dab456..031d5f2 100644 --- a/src/Adapter/Cache/PSR6Cache/composer.json +++ b/src/Adapter/Cache/PSR6Cache/composer.json @@ -21,18 +21,9 @@ "psr/cache": "^1.0 || ^2.0 || ^3.0", "secretary/core": "self.version" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, "autoload": { "psr-4": { "Secretary\\Adapter\\Cache\\PSR6Cache\\": "" } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Adapter\\Cache\\PSR6Cache\\Tests\\": "Tests/" - } } } diff --git a/src/Adapter/Chain/composer.json b/src/Adapter/Chain/composer.json index 78950ac..ddbfff2 100644 --- a/src/Adapter/Chain/composer.json +++ b/src/Adapter/Chain/composer.json @@ -19,18 +19,9 @@ "php": "^8.2", "secretary/core": "self.version" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, "autoload": { "psr-4": { "Secretary\\Adapter\\Chain\\": "" } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Adapter\\Chain\\Tests\\": "Tests/" - } } } diff --git a/src/Adapter/GCP/SecretsManager/composer.json b/src/Adapter/GCP/SecretsManager/composer.json index 49f39a2..be7c36c 100644 --- a/src/Adapter/GCP/SecretsManager/composer.json +++ b/src/Adapter/GCP/SecretsManager/composer.json @@ -23,18 +23,9 @@ "google/cloud-secret-manager": "^1.0", "secretary/core": "^3.0" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, "autoload": { "psr-4": { "Secretary\\Adapter\\GCP\\SecretsManager\\": "" } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Adapter\\GCP\\SecretsManager\\Tests\\": "Tests/" - } } } diff --git a/src/Adapter/Hashicorp/Vault/composer.json b/src/Adapter/Hashicorp/Vault/composer.json index a1ba58c..972e7f4 100644 --- a/src/Adapter/Hashicorp/Vault/composer.json +++ b/src/Adapter/Hashicorp/Vault/composer.json @@ -21,18 +21,9 @@ "guzzlehttp/guzzle": "^7.0", "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, "autoload": { "psr-4": { "Secretary\\Adapter\\Hashicorp\\Vault\\": "" } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Adapter\\Hashicorp\\Vault\\Tests\\": "Tests/" - } } } diff --git a/src/Adapter/Local/JSONFile/composer.json b/src/Adapter/Local/JSONFile/composer.json index 845e12e..96719b3 100644 --- a/src/Adapter/Local/JSONFile/composer.json +++ b/src/Adapter/Local/JSONFile/composer.json @@ -20,18 +20,9 @@ "ext-json": "*", "secretary/core": "self.version" }, - "require-dev": { - "mockery/mockery": "^1.6.12", - "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0 || ^13.0" - }, "autoload": { "psr-4": { "Secretary\\Adapter\\Local\\JSONFile\\": "" } - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Tests\\": "Tests/" - } } } diff --git a/src/Bundle/SecretaryBundle/.gitattributes b/src/Bundle/SecretaryBundle/.gitattributes new file mode 100644 index 0000000..3d069f6 --- /dev/null +++ b/src/Bundle/SecretaryBundle/.gitattributes @@ -0,0 +1 @@ +Tests export-ignore diff --git a/src/Bundle/SecretaryBundle/Test.php b/src/Bundle/SecretaryBundle/Test.php deleted file mode 100644 index 7cb69a2..0000000 --- a/src/Bundle/SecretaryBundle/Test.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @date 2019 - * @license https://opensource.org/licenses/MIT - */ - -require_once __DIR__.'/vendor/autoload.php'; - -use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * @author Aaron Scherer - * @date 2019 - * - * @license http://opensource.org/licenses/MIT - * - * @internal - * @coversNothing - */ -class Test extends \Symfony\Component\HttpKernel\Kernel -{ - use MicroKernelTrait; - public const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - /** - * Returns an array of bundles to register. - * - * @return iterable|\Symfony\Component\HttpKernel\Bundle\BundleInterface An iterable of bundle instances - */ - public function registerBundles() - { - yield new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(); - yield new \Secretary\Bundle\SecretaryBundle\SecretaryBundle(); - } - - /** - * Add or import routes into your application. - * - * $routes->import('config/routing.yml'); - * $routes->add('/admin', 'App\Controller\AdminController::dashboard', 'admin_dashboard'); - * - */ - protected function configureRoutes(Symfony\Component\Routing\RouteCollectionBuilder $routes) - { - } - - /** - * {@inheritDc}. - * - * @throws Exception - */ - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) - { - $loader->load(__DIR__.'/services'.self::CONFIG_EXTS, 'glob'); - } -} - -$k = new Test('dev', true); -$k->boot(); -var_dump($k->getContainer()->getParameter('foo')); diff --git a/src/Bundle/SecretaryBundle/Tests/SecretaryBundleTest.php b/src/Bundle/SecretaryBundle/Tests/SecretaryBundleTest.php new file mode 100644 index 0000000..ce4cb25 --- /dev/null +++ b/src/Bundle/SecretaryBundle/Tests/SecretaryBundleTest.php @@ -0,0 +1,144 @@ + + * @date 2019 + * @license https://opensource.org/licenses/MIT + */ + +namespace Secretary\Tests; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; +use Secretary\Bundle\SecretaryBundle\DependencyInjection\SecretaryExtension; +use Secretary\Bundle\SecretaryBundle\EnvVar\EnvVarProcessor; +use Secretary\Bundle\SecretaryBundle\SecretaryBundle; +use Secretary\Manager; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +#[CoversClass(SecretaryBundle::class)] +#[CoversClass(SecretaryExtension::class)] +class SecretaryBundleTest extends TestCase +{ + public function testBundleRegistersExtension(): void + { + $bundle = new SecretaryBundle(); + + $this->assertInstanceOf(SecretaryBundle::class, $bundle); + } + + public function testExtensionRegistersServices(): void + { + $container = new ContainerBuilder(); + $extension = new SecretaryExtension(); + + $extension->load([ + [ + 'adapters' => [ + 'default' => [ + 'adapter' => 'Secretary\Adapter\AWS\SecretsManager\AWSSecretsManagerAdapter', + 'config' => [ + 'region' => 'us-east-1', + 'version' => '2017-10-17', + ], + 'cache' => [ + 'enabled' => false, + ], + ], + ], + ], + ], $container); + + $this->assertTrue($container->has('secretary.adapter.default')); + $this->assertTrue($container->has('secretary.manager.default')); + $this->assertTrue($container->has('secretary')); + $this->assertTrue($container->has(Manager::class)); + $this->assertTrue($container->has('secretary.env_var_processor')); + } + + public function testExtensionRegistersMultipleAdapters(): void + { + $container = new ContainerBuilder(); + $extension = new SecretaryExtension(); + + $extension->load([ + [ + 'adapters' => [ + 'aws' => [ + 'adapter' => 'Secretary\Adapter\AWS\SecretsManager\AWSSecretsManagerAdapter', + 'config' => [ + 'region' => 'us-east-1', + 'version' => '2017-10-17', + ], + 'cache' => [ + 'enabled' => false, + ], + ], + 'default' => [ + 'adapter' => 'Secretary\Adapter\Chain\ChainAdapter', + 'config' => [], + 'cache' => [ + 'enabled' => false, + ], + ], + ], + ], + ], $container); + + $this->assertTrue($container->has('secretary.adapter.aws')); + $this->assertTrue($container->has('secretary.manager.aws')); + $this->assertTrue($container->has('secretary.adapter.default')); + $this->assertTrue($container->has('secretary.manager.default')); + // 'default' adapter should be aliased to 'secretary' + $this->assertTrue($container->has('secretary')); + } + + public function testFirstAdapterBecomesDefaultWhenNoDefaultDefined(): void + { + $container = new ContainerBuilder(); + $extension = new SecretaryExtension(); + + $extension->load([ + [ + 'adapters' => [ + 'aws' => [ + 'adapter' => 'Secretary\Adapter\AWS\SecretsManager\AWSSecretsManagerAdapter', + 'config' => [], + 'cache' => [ + 'enabled' => false, + ], + ], + ], + ], + ], $container); + + // 'aws' should be aliased as the default since no 'default' adapter exists + $alias = $container->getAlias('secretary'); + $this->assertEquals('secretary.manager.aws', (string) $alias); + } + + public function testEnvVarProcessorIsRegistered(): void + { + $container = new ContainerBuilder(); + $extension = new SecretaryExtension(); + + $extension->load([ + [ + 'adapters' => [ + 'default' => [ + 'adapter' => 'Secretary\Adapter\AWS\SecretsManager\AWSSecretsManagerAdapter', + 'config' => [], + 'cache' => [ + 'enabled' => false, + ], + ], + ], + ], + ], $container); + + $definition = $container->getDefinition('secretary.env_var_processor'); + $this->assertEquals(EnvVarProcessor::class, $definition->getClass()); + } +} diff --git a/src/Bundle/SecretaryBundle/composer.json b/src/Bundle/SecretaryBundle/composer.json index efda50b..d2745f7 100644 --- a/src/Bundle/SecretaryBundle/composer.json +++ b/src/Bundle/SecretaryBundle/composer.json @@ -30,8 +30,8 @@ "symfony/yaml": "^5.0 || ^6.0 || ^7.0 || ^8.0", "aws/aws-sdk-php": "^3.91" }, - "autoload": { - "psr-4": { + "autoload": { + "psr-4": { "Secretary\\Bundle\\SecretaryBundle\\": "" }, "exclude-from-classmap": [ @@ -40,7 +40,7 @@ }, "autoload-dev": { "psr-4": { - "Secretary\\Bundle\\SecretaryBundle\\Tests\\": "tests/" + "Secretary\\Tests\\": "Tests/" } } } diff --git a/src/Core/composer.json b/src/Core/composer.json index 12da30c..039f38a 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -21,10 +21,6 @@ "php": "^8.2", "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, - "require-dev": { - "phpunit/phpunit": "^10.5 || ^11.0", - "mockery/mockery": "^1.6.12" - }, "suggest": { "secretary/aws-secrets-manager-adapter": "For reading secrets from AWS Secrets Manager", "secretary/hashicorp-vault-adapter": "For reading secrets from Hashicorp Vault", @@ -39,10 +35,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "autoload-dev": { - "psr-4": { - "Secretary\\Tests\\": "Tests/" - } } }