*/ private array $_originalAppConfig; /** * Repository root used to resolve actual application scripts. * * @var string */ private string $_repoRoot; /** * Prepare an isolated workspace for each test. * * @return void */ protected function setUp(): void { parent::setUp(); global $appConfig; $this->_originalAppConfig = is_array($appConfig) ? $appConfig : []; $this->_repoRoot = (string) ($this->_originalAppConfig['base_path'] ?? dirname(__DIR__, 2)); $this->tempRoot = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ikb-tests-' . bin2hex(random_bytes(6)); $this->projectRoot = $this->tempRoot . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'projects'; mkdir($this->projectRoot, 0777, true); $appConfig['base_path'] = $this->tempRoot; $appConfig['project_root'] = $this->projectRoot; } /** * Restore the original workspace after the test. * * @return void */ protected function tearDown(): void { global $appConfig; $appConfig = $this->_originalAppConfig; $this->_removeDirectory($this->tempRoot); parent::tearDown(); } /** * Create a markdown-backed project for tests. * * @param string $projectId Project identifier. * @param string $title Project title. * @param string $body Project body content. * * @return string */ protected function createProject(string $projectId = 'demo-project', string $title = 'Demo Project', string $body = 'Project body'): string { $projectPath = $this->projectRoot . DIRECTORY_SEPARATOR . $projectId; mkdir($projectPath, 0777, true); file_put_contents( $projectPath . DIRECTORY_SEPARATOR . 'index.md', FrontMatter::dump( [ 'type' => 'project', 'id' => $projectId, 'title' => $title, ], $body ) ); return $projectId; } /** * Invoke an API endpoint script with request globals. * * @param string $script Relative script path from repo root. * @param string $method Request method. * @param array $get Query parameters. * @param array $post Form payload. * @param array $server Additional server variables. * * @return array{status:int, payload:array, raw:string} */ protected function callApi( string $script, string $method = 'GET', array $get = [], array $post = [], array $server = [] ): array { $_GET = $get; $_POST = $post; $_SERVER = array_merge( [ 'REQUEST_METHOD' => strtoupper($method), ], $server ); http_response_code(200); if (function_exists('header_remove')) { header_remove(); } ob_start(); try { include $this->_repoRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $script); } catch (HttpHalt $halt) { } $raw = (string) ob_get_clean(); $decoded = json_decode($raw, true); self::assertIsArray($decoded, 'Expected a JSON object response from ' . $script . '.'); return [ 'status' => http_response_code(), 'payload' => $decoded, 'raw' => $raw, ]; } /** * Recursively remove a directory tree. * * @param string $path Directory path. * * @return void */ private function _removeDirectory(string $path): void { if ($path === '' || !is_dir($path)) { return; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $item) { if ($item->isDir()) { rmdir($item->getPathname()); continue; } unlink($item->getPathname()); } rmdir($path); } }