diff --git a/.vscode/settings.json b/.vscode/settings.json index 4bf57ae..a37cb84 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,6 @@ "statusBar.debuggingForeground": "#F3FBFE", "statusBar.noFolderBackground": "#053241", "statusBar.noFolderForeground": "#F3FBFE" - } + }, + "phpcs.standard": ".\\phpcs.xml", } diff --git a/composer.json b/composer.json index 3017a05..0c69478 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ } }, "scripts": { - "test": "phpunit" + "test": "phpunit", + "lint": "phpcs --standard=phpcs.xml --report=full --report-file=phpcs-results.txt .", + "lint:fix": "phpcbf --standard=phpcs.xml ." } } diff --git a/src/Domain/Task.php b/src/Domain/Task.php index 96bf730..e154bda 100644 --- a/src/Domain/Task.php +++ b/src/Domain/Task.php @@ -1,5 +1,4 @@ getMessage(), 0, @@ -110,12 +106,11 @@ class FrontMatterParser /** * Dump a document to markdown string with front matter * - * @param FrontMatterDocument $document The document to dump + * @param FrontMatterDocument $document The document to dump. * - * @return string The markdown content with front matter + * @return string The markdown content with front matter. */ - public function dump(FrontMatterDocument $document): string - { + public function dump( FrontMatterDocument $document ): string { $yaml = Yaml::dump( $document->meta, 4, diff --git a/src/Repository/TaskRepository.php b/src/Repository/TaskRepository.php index afeee1b..7915850 100644 --- a/src/Repository/TaskRepository.php +++ b/src/Repository/TaskRepository.php @@ -1,5 +1,4 @@ parseString($contents); + + $this->assertSame('task-123', $document->meta['id']); + $this->assertSame('Test Task', $document->meta['title']); + $this->assertFalse($document->meta['completed']); + $this->assertSame(['alpha', 'beta'], $document->meta['tags']); + $this->assertStringContainsString('# Hello', $document->body); + } + + public function testReturnsEmptyMetaWhenNoFrontMatterExists(): void { + $contents = <<parseString($contents); + + $this->assertSame([], $document->meta); + $this->assertStringContainsString('# Plain Markdown', $document->body); + } + + public function testRoundTripsUnknownMetaKeys(): void { + $document = new FrontMatterDocument( + meta: [ + 'id' => 'task-456', + 'type' => 'task', + 'custom_key' => 'custom value', + 'nested' => [ + 'one' => 'two', + ], + ], + body: "Test body\n\nMore text." + ); + + $parser = new FrontMatterParser(); + $dumped = $parser->dump($document); + $parsed = $parser->parseString($dumped); + + $this->assertSame('custom value', $parsed->meta['custom_key']); + $this->assertSame(['one' => 'two'], $parsed->meta['nested']); + $this->assertSame("Test body\n\nMore text.", $parsed->body); + } +} +// phpcs:enable diff --git a/tests/TaskRepositoryTest.php b/tests/TaskRepositoryTest.php new file mode 100644 index 0000000..c604b79 --- /dev/null +++ b/tests/TaskRepositoryTest.php @@ -0,0 +1,147 @@ +tempRoot = sys_get_temp_dir() . '/ironkanban-test-' . bin2hex(random_bytes(4)); + mkdir($this->tempRoot, 0775, true); + mkdir($this->tempRoot . '/sample-project', 0775, true); + mkdir($this->tempRoot . '/sample-project/tasks', 0775, true); + } + + protected function tearDown(): void { + $this->deleteDirectory($this->tempRoot); + parent::tearDown(); + } + + public function testCreateWritesTaskFileAndCanReadItBack(): void { + $repo = $this->makeRepository(); + + $task = $repo->create([ + 'projectId' => 'sample-project', + 'title' => 'Build parser', + 'body' => "Task body", + 'column' => 'doing', + 'priority' => 'high', + ]); + + $this->assertSame('Build parser', $task->title); + $this->assertSame('doing', $task->column); + $this->assertSame('high', $task->priority); + $this->assertFileExists($this->tempRoot . '/sample-project/tasks/' . $task->fileName); + + $loaded = $repo->get('sample-project', $task->id); + + $this->assertSame($task->id, $loaded->id); + $this->assertSame('Build parser', $loaded->title); + $this->assertSame('Task body', $loaded->body); + $this->assertSame($task->fileName, $loaded->fileName); + } + + public function testPreservesLegacyFilenameWhenReadingAndSaving(): void { + $contents = <<tempRoot . '/sample-project/tasks/task-legacy-import.md'; + file_put_contents($legacyPath, $contents); + + $repo = $this->makeRepository(); + $task = $repo->get('sample-project', 'custom-task-id'); + + $this->assertSame('task-legacy-import.md', $task->fileName); + $this->assertSame('back-burner', $task->column); + $this->assertSame('keep-me', $task->meta['legacy_key']); + + $task->title = 'Legacy Task Updated'; + $repo->save($task); + + $this->assertFileExists($legacyPath); + $this->assertStringContainsString('Legacy Task Updated', (string) file_get_contents($legacyPath)); + $this->assertStringContainsString('legacy_key: keep-me', (string) file_get_contents($legacyPath)); + } + + public function testDeleteRemovesUnderlyingFile(): void { + $repo = $this->makeRepository(); + + $task = $repo->create([ + 'projectId' => 'sample-project', + 'title' => 'Delete me', + ]); + + $filePath = $this->tempRoot . '/sample-project/tasks/' . $task->fileName; + + $this->assertFileExists($filePath); + + $repo->delete('sample-project', $task->id); + + $this->assertFileDoesNotExist($filePath); + } + + private function makeRepository(): TaskRepository { + return new TaskRepository( + new Paths($this->tempRoot), + new FrontMatterParser(), + new TaskMapper(), + new IdGenerator() + ); + } + + private function deleteDirectory(string $path): void { + if (!is_dir($path)) { + return; + } + + $items = scandir($path); + + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $itemPath = $path . DIRECTORY_SEPARATOR . $item; + + if (is_dir($itemPath) && !is_link($itemPath)) { + $this->deleteDirectory($itemPath); + continue; + } + + @unlink($itemPath); + } + + @rmdir($path); + } +} +// phpcs:enable diff --git a/vendor/bin/.phpunit.result.cache b/vendor/bin/.phpunit.result.cache new file mode 100644 index 0000000..33b0f86 --- /dev/null +++ b/vendor/bin/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":2,"defects":{"IronKanban\\Tests\\TaskRepositoryTest::testCreateWritesTaskFileAndCanReadItBack":7,"IronKanban\\Tests\\TaskRepositoryTest::testPreservesLegacyFilenameWhenReadingAndSaving":7,"IronKanban\\Tests\\TaskRepositoryTest::testDeleteRemovesUnderlyingFile":7},"times":{"IronKanban\\Tests\\FrontMatterParserTest::testParsesFrontMatterAndBody":0.702,"IronKanban\\Tests\\FrontMatterParserTest::testReturnsEmptyMetaWhenNoFrontMatterExists":0,"IronKanban\\Tests\\FrontMatterParserTest::testRoundTripsUnknownMetaKeys":0.081,"IronKanban\\Tests\\TaskRepositoryTest::testCreateWritesTaskFileAndCanReadItBack":0.879,"IronKanban\\Tests\\TaskRepositoryTest::testPreservesLegacyFilenameWhenReadingAndSaving":0.037,"IronKanban\\Tests\\TaskRepositoryTest::testDeleteRemovesUnderlyingFile":0.009}} \ No newline at end of file