From cf0c5309a53641fd65a7db99c26d6f5ffe32d770 Mon Sep 17 00:00:00 2001 From: Aaron Francis Date: Mon, 24 May 2021 16:48:04 -0500 Subject: [PATCH] Configure Command Test (#10) * Finally write a happy path test for the configure command. #8 & #9 * Apply fixes from StyleCI --- CHANGELOG.md | 4 + src/Commands/Actions/CreateBucket.php | 3 +- src/Commands/Actions/CreateDeploymentUser.php | 8 +- src/Commands/Actions/CreateExecutionRole.php | 4 +- src/Commands/Actions/DestroyAdminKeys.php | 16 +- src/Commands/Configure.php | 16 +- tests/Commands/ConfigureTest.php | 317 +++++++++++++++++- 7 files changed, 351 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3fb62..13c913f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## 0.1.2 - 2021-05-24 + +- Fix other `sidecar:configure` AWS errors. ([#8](https://github.com/hammerstonedev/sidecar/issues/8) & ([#9](https://github.com/hammerstonedev/sidecar/issues/9)) + ## 0.1.1 - 2021-05-24 - Fix undefined `choice` ([#7](https://github.com/hammerstonedev/sidecar/issues/7)) diff --git a/src/Commands/Actions/CreateBucket.php b/src/Commands/Actions/CreateBucket.php index 3ed84fe..2a45d4e 100644 --- a/src/Commands/Actions/CreateBucket.php +++ b/src/Commands/Actions/CreateBucket.php @@ -7,6 +7,7 @@ use Aws\S3\S3Client; use Illuminate\Support\Str; +use Throwable; class CreateBucket extends BaseAction { @@ -27,7 +28,7 @@ public function invoke() { $this->client = $this->command->client(S3Client::class); - $this->bucket = config('sidecar.aws_bucket', $this->defaultBucketName()); + $this->bucket = config('sidecar.aws_bucket') ?? $this->defaultBucketName(); $this->ensureBucketIsPrefixed(); diff --git a/src/Commands/Actions/CreateDeploymentUser.php b/src/Commands/Actions/CreateDeploymentUser.php index d84473e..ba2a4bd 100644 --- a/src/Commands/Actions/CreateDeploymentUser.php +++ b/src/Commands/Actions/CreateDeploymentUser.php @@ -6,6 +6,8 @@ namespace Hammerstone\Sidecar\Commands\Actions; use Aws\Iam\IamClient; +use Illuminate\Support\Arr; +use Throwable; class CreateDeploymentUser extends BaseAction { @@ -113,7 +115,7 @@ protected function issueCredentials() 'UserName' => 'sidecar-deployment-user', ]); - if (!$keys->count()) { + if (!count($keys)) { return $this->createAccessKey(); } @@ -149,8 +151,8 @@ protected function createAccessKey() ]); return [ - 'key' => $result->search('AccessKey.AccessKeyId'), - 'secret' => $result->search('AccessKey.SecretAccessKey'), + 'key' => Arr::get($result, 'AccessKey.AccessKeyId'), + 'secret' => Arr::get($result, 'AccessKey.SecretAccessKey'), ]; } } diff --git a/src/Commands/Actions/CreateExecutionRole.php b/src/Commands/Actions/CreateExecutionRole.php index efc3ae5..186ec23 100644 --- a/src/Commands/Actions/CreateExecutionRole.php +++ b/src/Commands/Actions/CreateExecutionRole.php @@ -6,6 +6,8 @@ namespace Hammerstone\Sidecar\Commands\Actions; use Aws\Iam\IamClient; +use Illuminate\Support\Arr; +use Throwable; class CreateExecutionRole extends BaseAction { @@ -20,7 +22,7 @@ public function invoke() $this->client = $this->command->client(IamClient::class); - $role = $this->findOrCreateRole()->search('Role.Arn'); + $role = Arr::get($this->findOrCreateRole(), 'Role.Arn'); $this->attachPolicy(); diff --git a/src/Commands/Actions/DestroyAdminKeys.php b/src/Commands/Actions/DestroyAdminKeys.php index 37dea06..b77ff61 100644 --- a/src/Commands/Actions/DestroyAdminKeys.php +++ b/src/Commands/Actions/DestroyAdminKeys.php @@ -6,10 +6,20 @@ namespace Hammerstone\Sidecar\Commands\Actions; use Aws\Iam\IamClient; +use Throwable; class DestroyAdminKeys extends BaseAction { - public function invoke($key) + public $key; + + public function setKey($key) + { + $this->key = $key; + + return $this; + } + + public function invoke() { $client = $this->command->client(IamClient::class); @@ -25,7 +35,7 @@ public function invoke($key) "Now that everything is setup, would you like to remove the admin access keys for user `$name` from AWS? \n" . ' Sidecar no longer needs them.'; - if (!$this->confirm($question, $default = true)) { + if (!$this->command->confirm($question, $default = true)) { return; } @@ -33,7 +43,7 @@ public function invoke($key) try { $client->deleteAccessKey([ - 'AccessKeyId' => $key, + 'AccessKeyId' => $this->key, 'UserName' => $name, ]); } catch (Throwable $e) { diff --git a/src/Commands/Configure.php b/src/Commands/Configure.php index 9d2f280..1a6b09a 100644 --- a/src/Commands/Configure.php +++ b/src/Commands/Configure.php @@ -64,7 +64,7 @@ public function handle() $credentials = $this->action(CreateDeploymentUser::class)->invoke(); - $this->action(DestroyAdminKeys::class)->invoke($this->key); + $this->action(DestroyAdminKeys::class)->setKey($this->key)->invoke(); $this->line(' '); $this->info('Done! Here are your environment variables:'); @@ -84,12 +84,14 @@ public function text($text) public function client($class) { - return new $class([ - 'region' => $this->region, - 'version' => 'latest', - 'credentials' => [ - 'key' => $this->key, - 'secret' => $this->secret + return app()->make($class, [ + 'args' => [ + 'region' => $this->region, + 'version' => 'latest', + 'credentials' => [ + 'key' => $this->key, + 'secret' => $this->secret + ] ] ]); } diff --git a/tests/Commands/ConfigureTest.php b/tests/Commands/ConfigureTest.php index c4f6663..e224a20 100644 --- a/tests/Commands/ConfigureTest.php +++ b/tests/Commands/ConfigureTest.php @@ -5,11 +5,324 @@ namespace Hammerstone\Sidecar\Tests; +use Aws\Command; +use Aws\Iam\Exception\IamException; +use Aws\Iam\IamClient; +use Aws\S3\Exception\S3Exception; +use Aws\S3\S3Client; +use Illuminate\Support\Carbon; +use Mockery; + class ConfigureTest extends BaseTest { + protected function setUp(): void + { + parent::setUp(); + + Carbon::setTestNow('2021-05-24 00:00:00'); + } + + protected function mockS3($callable) + { + $mock = Mockery::mock(S3Client::class, $callable); + + $this->app->singleton(S3Client::class, function () use ($mock) { + return $mock; + }); + } + + protected function mockIam($callable) + { + $mock = Mockery::mock(IamClient::class, $callable); + + $this->app->singleton(IamClient::class, function () use ($mock) { + return $mock; + }); + } + + protected function mockHeadBucketNotFound($mock) + { + $mock->shouldReceive('headBucket') + ->once() + ->with([ + 'Bucket' => 'sidecar-us-east-1-1621814400', + ]) + ->andThrow($this->bucketNotFoundException()); + } + + protected function bucketNotFoundException() + { + return new S3Exception('403 Forbidden', new Command('headBucket')); + } + + protected function mockCreateBucket($mock) + { + $mock->shouldReceive('createBucket') + ->once() + ->with([ + 'ACL' => 'private', + 'Bucket' => 'sidecar-us-east-1-1621814400', + 'CreateBucketConfiguration' => [ + 'LocationConstraint' => 'us-east-1', + ], + ]); + } + + protected function mockRoleNotFound($mock) + { + $mock->shouldReceive('getRole') + ->once() + ->with([ + 'RoleName' => 'sidecar-execution-role' + ]) + ->andThrow( + new IamException('Not found', new Command('GetRole')) + ); + } + + protected function mockCreateRole($mock) + { + $mock->shouldReceive('createRole') + ->once() + ->with([ + 'RoleName' => 'sidecar-execution-role', + 'AssumeRolePolicyDocument' => json_encode([ + 'Version' => '2012-10-17', + 'Statement' => [[ + 'Effect' => 'Allow', + 'Principal' => [ + 'Service' => 'lambda.amazonaws.com' + ], + 'Action' => 'sts:AssumeRole' + ]] + ]) + ]) + ->andReturn($this->roleExistsResponse()); + } + + protected function mockPuttingPolicy($mock) + { + $mock->shouldReceive('putRolePolicy') + ->once() + ->with([ + 'PolicyName' => 'sidecar-execution-policy', + 'RoleName' => 'sidecar-execution-role', + 'PolicyDocument' => json_encode([ + 'Version' => '2012-10-17', + 'Statement' => [[ + 'Effect' => 'Allow', + 'Resource' => '*', + 'Action' => [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:FilterLogEvents', + 'logs:PutLogEvents', + 'lambda:invokeFunction', + 's3:*', + 'ses:*', + 'sqs:*', + 'dynamodb:*' + ], + ]] + ]), + ]); + } + + protected function roleExistsResponse() + { + return [ + 'Role' => [ + 'Path' => '/', + 'RoleName' => 'sidecar-execution-role', + 'RoleId' => 'XXX', + 'Arn' => 'arn:aws:iam::XXX:role/sidecar-execution-role', + 'AssumeRolePolicyDocument' => '%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22lambda.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D', + 'MaxSessionDuration' => 3600, + ], + ]; + } + + protected function createRolePayload() + { + return [ + 'RoleName' => 'sidecar-execution-role', + 'AssumeRolePolicyDocument' => json_encode([ + 'Version' => '2012-10-17', + 'Statement' => [[ + 'Effect' => 'Allow', + 'Principal' => [ + 'Service' => 'lambda.amazonaws.com' + ], + 'Action' => 'sts:AssumeRole' + ]] + ]), + ]; + } + /** @test */ - public function test_todo() + public function basic_happy_path() + { + $this->mockS3(function ($mock) { + $this->mockHeadBucketNotFound($mock); + $this->mockCreateBucket($mock); + }); + + $this->mockIam(function ($mock) { + $this->mockRoleNotFound($mock); + $this->mockCreateRole($mock); + $this->mockPuttingPolicy($mock); + $this->mockDeploymentUserNotFound($mock); + $this->mockCreateDeploymentUser($mock); + $this->mockPutDeploymentUserPolicy($mock); + $this->mockListAccessKeys($mock); + $this->mockCreateAccessKeys($mock); + $this->mockWhoAmI($mock); + }); + + $artisan = $this->artisan('sidecar:configure'); + + $artisan->expectsQuestion('Enter the Access key ID', 'id'); + $artisan->expectsQuestion('Enter the Secret access key', 'secret'); + $artisan->expectsQuestion('What AWS region would you like your functions to be deployed in?', 'us-east-1'); + + $artisan->expectsOutput('==> Bucket doesn\'t exist. Creating...'); + $artisan->expectsOutput('==> Bucket created'); + $artisan->expectsOutput('==> Creating an execution role for your functions...'); + $artisan->expectsOutput('==> Attaching policy to execution role...'); + $artisan->expectsOutput('==> Creating deployment user...'); + $artisan->expectsOutput('==> Creating new keys...'); + + $artisan->expectsQuestion( + 'Now that everything is setup, would you like to remove the admin access keys for user `sidecar-cli-helper` from AWS?' . + " \n Sidecar no longer needs them.", + false + ); + + $artisan->expectsOutput('SIDECAR_ACCESS_KEY_ID=AK-XXXX'); + $artisan->expectsOutput('SIDECAR_SECRET_ACCESS_KEY=SK-XXXX'); + $artisan->expectsOutput('SIDECAR_REGION=us-east-1'); + $artisan->expectsOutput('SIDECAR_ARTIFACT_BUCKET_NAME=sidecar-us-east-1-1621814400'); + $artisan->expectsOutput('SIDECAR_EXECUTION_ROLE=arn:aws:iam::XXX:role/sidecar-execution-role'); + } + + /** + * @param $mock + */ + protected function mockDeploymentUserNotFound($mock): void + { + $mock->shouldReceive('getUser') + ->once() + ->with([ + 'UserName' => 'sidecar-deployment-user' + ]) + ->andThrow( + new IamException('Not found', new Command('getUser')) + ); + } + + /** + * @param $mock + */ + protected function mockCreateDeploymentUser($mock): void + { + $mock->shouldReceive('createUser') + ->once() + ->with([ + 'UserName' => 'sidecar-deployment-user' + ]); + } + + /** + * @param $mock + */ + protected function mockPutDeploymentUserPolicy($mock): void + { + $mock->shouldReceive('putUserPolicy') + ->once() + ->with([ + 'PolicyName' => 'sidecar-deployment-policy', + 'UserName' => 'sidecar-deployment-user', + 'PolicyDocument' => json_encode([ + 'Version' => '2012-10-17', + 'Statement' => [[ + 'Effect' => 'Allow', + 'Action' => 's3:*', + 'Resource' => [ + // We only need access to sidecar buckets. + 'arn:aws:s3:::sidecar-*', + 'arn:aws:s3:::sidecar-*/*', + ] + ], [ + 'Effect' => 'Allow', + 'Action' => [ + 'lambda:*', + 'states:*', + ], + 'Resource' => '*', + ], [ + 'Effect' => 'Allow', + 'Action' => 'iam:PassRole', + 'Resource' => '*', + 'Condition' => [ + 'StringEquals' => [ + 'iam:PassedToService' => 'lambda.amazonaws.com', + ], + ], + ], [ + 'Effect' => 'Allow', + 'Action' => [ + 'logs:DescribeLogStreams', + 'logs:GetLogEvents', + 'logs:FilterLogEvents', + ], + 'Resource' => 'arn:aws:logs:*:*:log-group:/aws/lambda/*', + ]], + ]), + ]); + } + + /** + * @param $mock + */ + protected function mockListAccessKeys($mock): void + { + $mock->shouldReceive('listAccessKeys') + ->once() + ->with([ + 'UserName' => 'sidecar-deployment-user', + ]) + ->andReturn([]); + } + + /** + * @param $mock + */ + protected function mockCreateAccessKeys($mock): void + { + $mock->shouldReceive('createAccessKey') + ->once() + ->with([ + 'UserName' => 'sidecar-deployment-user', + ]) + ->andReturn([ + 'AccessKey' => [ + 'AccessKeyId' => 'AK-XXXX', + 'SecretAccessKey' => 'SK-XXXX' + ] + ]); + } + + /** + * @param $mock + */ + protected function mockWhoAmI($mock): void { - $this->markTestIncomplete('Test the configuration command.'); + $mock->shouldReceive('getUser') + ->withNoArgs() + ->andReturn([ + 'User' => [ + 'UserName' => 'sidecar-cli-helper' + ] + ]); } }