packages/requirements/tests/Unit/Config/Markdown/FrontmatterTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Config\Markdown;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Config\Markdown\Frontmatter;
13use Requirements\Input\Fields;
14use Requirements\Input\InvalidInputException;
15use Symfony\Component\Yaml\Exception\ParseException;
16use Tests\Fake\ProjectDirectory;
17
18#[CoversClass(Frontmatter::class)]
19#[UsesClass(Fields::class)]
20#[Small]
21final class FrontmatterTest extends TestCase
22{
23 public function testReadSplitsTheFrontmatterFromTheBody(): void
24 {
25 $project = new ProjectDirectory();
26 $file = $project->put('definition.md', "---\nversion: 1\nsource: null\n---\n\n# SPEC-001\n\nThe reader shall emit a tree.\n");
27 [$data, $body] = (new Frontmatter())->read($file);
28 self::assertEquals((object) ['version' => 1, 'source' => null], $data);
29 self::assertSame("\n# SPEC-001\n\nThe reader shall emit a tree.\n", $body);
30 }
31
32 public function testReadAcceptsCarriageReturnLineFeeds(): void
33 {
34 $project = new ProjectDirectory();
35 $file = $project->put('definition.md', "---\r\nversion: 1\r\n---\r\n# SPEC-001\r\n");
36 [$data, $body] = (new Frontmatter())->read($file);
37 self::assertEquals((object) ['version' => 1], $data);
38 self::assertSame("# SPEC-001\r\n", $body);
39 }
40
41 public function testReadReadsTheSourceAsAMapping(): void
42 {
43 $project = new ProjectDirectory();
44 $file = $project->put('definition.md', "---\n\$schema: definition.document.yaml\nversion: 1\nsource:\n id: manual\n uri: source.html\n---\n");
45 [$data, $body] = (new Frontmatter())->read($file);
46 self::assertEquals((object) ['$schema' => 'definition.document.yaml', 'version' => 1, 'source' => (object) ['id' => 'manual', 'uri' => 'source.html']], $data);
47 self::assertSame('', $body);
48 }
49
50 #[DataProvider('providerWithoutFrontmatter')]
51 public function testReadRejectsAFileWithoutFrontmatter(string $text): void
52 {
53 $project = new ProjectDirectory();
54 $file = $project->put('definition.md', $text);
55 $this->expectException(InvalidInputException::class);
56 $this->expectExceptionMessage("$file: expected YAML frontmatter delimited by ---.");
57 (new Frontmatter())->read($file);
58 }
59
60 /**
61 * @return array<string, array{string}>
62 */
63 public static function providerWithoutFrontmatter(): array
64 {
65 return [
66 'no frontmatter' => ["# SPEC-001\n\nThe reader shall emit a tree.\n"],
67 'unclosed frontmatter' => ["---\nversion: 1\n# SPEC-001\n"],
68 'closing delimiter without line break' => ["---\nversion: 1\n---"],
69 'text before the frontmatter' => ["\n---\nversion: 1\n---\n"],
70 ];
71 }
72
73 #[DataProvider('providerNotMappings')]
74 public function testReadRejectsFrontmatterThatIsNotAMapping(string $yaml): void
75 {
76 $project = new ProjectDirectory();
77 $file = $project->put('definition.md', "---\n$yaml\n---\n");
78 $this->expectException(InvalidInputException::class);
79 $this->expectExceptionMessage("$file: frontmatter must be a mapping.");
80 (new Frontmatter())->read($file);
81 }
82
83 /**
84 * @return array<string, array{string}>
85 */
86 public static function providerNotMappings(): array
87 {
88 return [
89 'sequence' => ['- version'],
90 'scalar' => ['version'],
91 ];
92 }
93
94 public function testReadRejectsUnknownFrontmatterKeys(): void
95 {
96 $project = new ProjectDirectory();
97 $file = $project->put('definition.md', "---\nversion: 1\nitems: []\n---\n");
98 $this->expectException(InvalidInputException::class);
99 $this->expectExceptionMessage("$file frontmatter: unknown field 'items'.");
100 (new Frontmatter())->read($file);
101 }
102
103 public function testReadRejectsMalformedYaml(): void
104 {
105 $project = new ProjectDirectory();
106 $file = $project->put('definition.md', "---\nversion: [1\n---\n");
107 $this->expectException(ParseException::class);
108 (new Frontmatter())->read($file);
109 }
110}
111