*/ private array $env = []; protected function setUp(): void { $unique = uniqid('todo-test-', true); $this->databasePath = sys_get_temp_dir() . "/{$unique}.sqlite"; $this->mailLogPath = sys_get_temp_dir() . "/{$unique}.mail.log"; $this->setEnv([ 'DATABASE_PATH' => $this->databasePath, 'APP_URL' => 'https://app.test', 'MAIL_TRANSPORT' => 'log', 'MAIL_LOG_PATH' => $this->mailLogPath, ]); $pdo = $this->db(); $pdo->exec('PRAGMA foreign_keys = ON'); foreach (glob(dirname(__DIR__) . '/migrations/*.sql') ?: [] as $migration) { $pdo->exec((string) file_get_contents($migration)); } $this->app = require dirname(__DIR__) . '/src/bootstrap.php'; } protected function tearDown(): void { $this->db = null; @unlink($this->databasePath); @unlink($this->mailLogPath); foreach (array_keys($this->env) as $key) { putenv($key); unset($_ENV[$key], $_SERVER[$key]); } $this->env = []; } /** @param array $vars */ private function setEnv(array $vars): void { foreach ($vars as $key => $value) { putenv("{$key}={$value}"); $_ENV[$key] = $value; $_SERVER[$key] = $value; $this->env[$key] = $value; } } /** A connection to the test database, for seeding rows directly. */ protected function db(): PDO { if ($this->db === null) { $this->db = new PDO('sqlite:' . $this->databasePath, null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); } return $this->db; } /** * @param array|null $body * @param array $headers */ protected function request( string $method, string $path, ?array $body = null, array $headers = [], ): ResponseInterface { $request = (new ServerRequestFactory())->createServerRequest($method, $path); foreach ($headers as $name => $value) { $request = $request->withHeader($name, $value); } if ($body !== null) { $request = $request->withParsedBody($body)->withHeader('Content-Type', 'application/json'); } return $this->app->handle($request); } /** * Register a fresh user and return an `Authorization` header for them. * * @return array */ protected function authHeader(string $email = 'user@example.com'): array { $token = $this->decode( $this->request('POST', '/api/auth/register', ['email' => $email, 'password' => 'password123']), )['token']; return ['Authorization' => 'Bearer ' . $token]; } /** All emails sent so far, oldest first. @return list */ protected function sentEmails(): array { if (!is_file($this->mailLogPath)) { return []; } $lines = array_filter(explode("\n", (string) file_get_contents($this->mailLogPath))); return array_map( static fn (string $line): array => json_decode($line, true, 512, JSON_THROW_ON_ERROR), array_values($lines), ); } /** @return array{to: string, subject: string, body: string} */ protected function lastEmail(): array { $emails = $this->sentEmails(); self::assertNotEmpty($emails, 'Expected an email to have been sent.'); return $emails[array_key_last($emails)]; } /** Pull the magic-link token out of an email body. */ protected function tokenFromEmail(?array $email = null): string { $email ??= $this->lastEmail(); self::assertSame(1, preg_match('/verify-email\?token=([a-f0-9]+)/', $email['body'], $m)); return $m[1]; } /** * @return array */ protected function decode(ResponseInterface $response): array { return (array) json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR); } }