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