diff --git a/src/Presentation/Console/Commands/CreateClientCommand.php b/src/Presentation/Console/Commands/CreateClientCommand.php new file mode 100644 index 0000000..ce6a0fc --- /dev/null +++ b/src/Presentation/Console/Commands/CreateClientCommand.php @@ -0,0 +1,125 @@ +askForClientName(); + + if ($clientName === null) { + return self::FAILURE; + } + + if (! $this->confirmCreation($clientName)) { + $this->warn('Client creation cancelled.'); + + return self::SUCCESS; + } + + $input = new Input($clientName); + + /** @var CreateClientView $view */ + $view = UsecaseBuilder::using($executor) + ->do($usecase) + ->from($input) + ->with(new CreateClientPresenter()) + ->run(); + + $this->displayCredentials($view); + + return self::SUCCESS; + } + + private function askForClientName(): ?ClientName + { + $maxAttempts = 3; + + for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { + /** @var string|null $nameInput */ + $nameInput = $this->ask('Client name (e.g. "Ones Branding website")'); + $nameInput = $nameInput !== null ? trim($nameInput) : ''; + + if ($nameInput === '') { + $this->error('Client name cannot be empty.'); + + continue; + } + + try { + return new ClientName($nameInput); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage()); + + if ($attempt < $maxAttempts) { + $this->warn(sprintf('You have %d attempt(s) remaining.', $maxAttempts - $attempt)); + } + } + } + + $this->error('Maximum number of attempts reached. Aborting.'); + + return null; + } + + private function confirmCreation(ClientName $name): bool + { + $this->newLine(); + $this->info('Summary:'); + $this->table( + ['Field', 'Value'], + [ + ['Name', $name->value], + ['Identifier', ''], + ['Secret', ''], + ['Status', 'Active'], + ], + ); + + return $this->confirm('Create this API client?', false); + } + + private function displayCredentials(CreateClientView $view): void + { + $this->newLine(); + $this->info($view->message); + $this->newLine(); + + $this->table( + ['Field', 'Value'], + [ + ['ID', $view->id], + ['Name', $view->name], + ['Identifier', $view->identifier], + ['Secret', $view->secret], + ], + ); + + $this->newLine(); + $this->warn('WARNING: Store the secret securely, it cannot be retrieved later.'); + } +} diff --git a/src/Presentation/Console/Presenters/Clients/CreateClientPresenter.php b/src/Presentation/Console/Presenters/Clients/CreateClientPresenter.php new file mode 100644 index 0000000..99f1e65 --- /dev/null +++ b/src/Presentation/Console/Presenters/Clients/CreateClientPresenter.php @@ -0,0 +1,28 @@ + */ +final readonly class CreateClientPresenter implements Presenter +{ + #[\Override] + public function present(UsecaseOutput $output): object + { + Assert::isInstanceOf($output, Output::class); + + return new CreateClientView( + id: $output->clientId->value, + name: $output->clientName->value, + identifier: $output->clientIdentifier->value, + secret: $output->clientSecret->value, + message: "API client '{$output->clientName->value}' created successfully.", + ); + } +} diff --git a/src/Presentation/Console/Presenters/Clients/CreateClientView.php b/src/Presentation/Console/Presenters/Clients/CreateClientView.php new file mode 100644 index 0000000..6dcaa9e --- /dev/null +++ b/src/Presentation/Console/Presenters/Clients/CreateClientView.php @@ -0,0 +1,17 @@ +artisan('client:create') + + // THEN + ->expectsQuestion('Client name (e.g. "Production Gateway")', $clientName) + ->expectsConfirmation('Create this API client?', 'yes') + ->expectsOutputToContain("API client '{$clientName}' created successfully.") + ->expectsOutputToContain('Store the secret securely') + ->assertSuccessful(); + + $client = ApiClient::query()->where('name', $clientName)->first(); + expect($client)->not->toBeNull() + ->and($client->is_active)->toBeTrue() + ->and($client->name->value)->toBe($clientName) + ->and((string) $client->identifier->value)->toStartWith('arcadia_id_') + ->and((string) $client->secret->value)->toStartWith('arcadia_sec_'); + }); + + it('aborts when user cancels confirmation', function (): void { + // GIVEN + /** @var \Tests\TestCase $this */ + $clientName = 'Cancelled Gateway'; + + // WHEN + $this->artisan('client:create') + + // THEN + ->expectsQuestion('Client name (e.g. "Production Gateway")', $clientName) + ->expectsConfirmation('Create this API client?', 'no') + ->expectsOutput('Client creation cancelled.') + ->assertSuccessful(); + + $client = ApiClient::query()->where('name', $clientName)->first(); + expect($client)->toBeNull(); + }); + + it('retries when an empty name is provided', function (): void { + // GIVEN + /** @var \Tests\TestCase $this */ + $this->artisan('client:create') + ->expectsQuestion('Client name (e.g. "Production Gateway")', '') + ->expectsOutput('Client name cannot be empty.') + ->expectsQuestion('Client name (e.g. "Production Gateway")', 'Valid Gateway') + + // WHEN + ->expectsConfirmation('Create this API client?', 'yes') + ->expectsOutputToContain("API client 'Valid Gateway' created successfully.") + + // THEN + ->assertSuccessful(); + + $client = ApiClient::query() + ->where('name', 'Valid Gateway') + ->first(); + expect($client)->not->toBeNull(); + }); + + it('fails after maximum retries with invalid names', function (): void { + // GIVEN + /** @var \Tests\TestCase $this */ + $this->artisan('client:create') + + // WHEN + ->expectsQuestion('Client name (e.g. "Production Gateway")', '') + ->expectsOutput('Client name cannot be empty.') + ->expectsQuestion('Client name (e.g. "Production Gateway")', '') + ->expectsOutput('Client name cannot be empty.') + ->expectsQuestion('Client name (e.g. "Production Gateway")', '') + ->expectsOutput('Client name cannot be empty.') + ->expectsOutput('Maximum number of attempts reached. Aborting.') + + // THEN + ->assertFailed(); + }); +});