users = $this->createMock(UserRepositoryInterface::class); $this->apiKeys = $this->createMock(ApiKeyRepositoryInterface::class); $this->tester = new CommandTester(new CreateApiKeyCommand($this->users, $this->apiKeys)); } public function testFailsWhenEmailIsEmpty(): void { $this->tester->setInputs(['', '']); $this->tester->execute([]); self::assertSame(1, $this->tester->getStatusCode()); self::assertStringContainsString('Email is required', $this->tester->getDisplay()); } public function testFailsWhenUserNotFound(): void { $this->users->method('findByEmail')->willReturn(null); $this->tester->setInputs(['unknown@example.com', '']); $this->tester->execute([]); self::assertSame(1, $this->tester->getStatusCode()); self::assertStringContainsString('No user found', $this->tester->getDisplay()); } public function testFailsWhenLabelIsEmpty(): void { $this->users->method('findByEmail')->willReturn(new User('test@example.com', 'hash')); $this->tester->setInputs(['test@example.com', '']); $this->tester->execute([]); self::assertSame(1, $this->tester->getStatusCode()); self::assertStringContainsString('Label is required', $this->tester->getDisplay()); } public function testCreatesKeyAndPrintsIt(): void { $user = new User('test@example.com', 'hash'); $this->users->method('findByEmail')->willReturn($user); $savedKey = null; $this->apiKeys->expects($this->once()) ->method('save') ->willReturnCallback(static function (ApiKey $key) use (&$savedKey): void { $savedKey = $key; }); $this->tester->setInputs(['test@example.com', 'dev laptop']); $this->tester->execute([]); self::assertSame(0, $this->tester->getStatusCode()); $display = $this->tester->getDisplay(); self::assertStringContainsString('API key created', $display); self::assertStringContainsString('dev laptop', $display); self::assertNotNull($savedKey); self::assertSame('dev laptop', $savedKey->getLabel()); self::assertSame(8, \strlen($savedKey->getKeyPrefix())); self::assertStringContainsString($savedKey->getKeyPrefix(), $display); } public function testStoredHashVerifiesAgainstPrintedKey(): void { $user = new User('test@example.com', 'hash'); $this->users->method('findByEmail')->willReturn($user); $savedKey = null; $this->apiKeys->method('save') ->willReturnCallback(static function (ApiKey $key) use (&$savedKey): void { $savedKey = $key; }); $this->tester->setInputs(['test@example.com', 'ci-runner']); $this->tester->execute([]); self::assertNotNull($savedKey); // SymfonyStyle table uses spaces, not pipes: " API Key " preg_match('/API Key\s+([a-f0-9]{48})/', $this->tester->getDisplay(), $matches); self::assertNotEmpty($matches, 'Raw key not found in output'); $rawKey = $matches[1]; self::assertSame(substr($rawKey, 0, 8), $savedKey->getKeyPrefix()); self::assertTrue(password_verify($rawKey, $savedKey->getKeyHash())); } }