packages/requirements/tests/Unit/Config/Markdown/MetadataReaderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Config\Markdown;
6
7use League\CommonMark\Exception\CommonMarkException;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\Small;
11use PHPUnit\Framework\Attributes\UsesClass;
12use PHPUnit\Framework\TestCase;
13use Requirements\Config\Markdown\MetadataReader;
14use Requirements\Config\Markdown\Nodes;
15use Requirements\Input\InvalidInputException;
16use stdClass;
17use Tests\Fake\MarkdownNodes;
18
19#[CoversClass(MetadataReader::class)]
20#[UsesClass(Nodes::class)]
21#[Small]
22final class MetadataReaderTest extends TestCase
23{
24 /**
25 * @throws CommonMarkException
26 */
27 public function testReadReadsAListOfKeysAndValues(): void
28 {
29 $markdown = "- **owner:** Parser team\n- **reviewed:** true\n- **priority:** 2\n- **text:** \"true\"\n- **values:**\n - stable\n - 1\n";
30 self::assertEquals((object) ['owner' => 'Parser team', 'reviewed' => true, 'priority' => 2, 'text' => 'true', 'values' => ['stable', 1]], (new MetadataReader())->read(MarkdownNodes::first($markdown)));
31 }
32
33 /**
34 * @throws CommonMarkException
35 */
36 public function testReadReadsAJsonObjectParagraph(): void
37 {
38 self::assertEquals((object) ['owner' => 'Parser team', 'values' => [1]], (new MetadataReader())->read(MarkdownNodes::first('{"owner": "Parser team", "values": [1]}')));
39 }
40
41 /**
42 * @throws CommonMarkException
43 */
44 public function testReadReadsAnEmptyJsonObjectParagraph(): void
45 {
46 self::assertEquals(new stdClass(), (new MetadataReader())->read(MarkdownNodes::first('{}')));
47 }
48
49 /**
50 * @throws CommonMarkException
51 */
52 #[DataProvider('providerNotMappings')]
53 public function testReadRejectsAValueThatIsNotAMapping(string $markdown): void
54 {
55 $this->expectException(InvalidInputException::class);
56 $this->expectExceptionMessage('Metadata must be a list of bold keys and values.');
57 (new MetadataReader())->read(MarkdownNodes::first($markdown));
58 }
59
60 /**
61 * @return array<string, array{string}>
62 */
63 public static function providerNotMappings(): array
64 {
65 return [
66 'text' => ['Parser team'],
67 'JSON array' => ['[1, 2]'],
68 'JSON number' => ['2'],
69 'sequence list' => ["- stable\n- 1\n"],
70 ];
71 }
72
73 /**
74 * @throws CommonMarkException
75 */
76 public function testNestedReadsAMapping(): void
77 {
78 self::assertEquals((object) ['a' => 1, 'b' => 'text'], (new MetadataReader())->nested(MarkdownNodes::first("- **a:** 1\n- **b:** text\n")));
79 }
80
81 /**
82 * @throws CommonMarkException
83 */
84 public function testNestedReadsASequence(): void
85 {
86 self::assertSame(['stable', 1, null, true], (new MetadataReader())->nested(MarkdownNodes::first("- stable\n- 1\n- null\n- true\n")));
87 }
88
89 /**
90 * @throws CommonMarkException
91 */
92 public function testNestedReadsNestedSequencesUnderAnEmptyListValue(): void
93 {
94 self::assertSame([['a', 'b'], []], (new MetadataReader())->nested(MarkdownNodes::first("- []\n - a\n - b\n- []\n")));
95 }
96
97 /**
98 * @throws CommonMarkException
99 */
100 public function testNestedReadsNestedMappings(): void
101 {
102 self::assertEquals((object) ['outer' => (object) ['inner' => [(object) ['value' => null]]]], (new MetadataReader())->nested(MarkdownNodes::first("- **outer:**\n - **inner:**\n - []\n - **value:** null\n")));
103 }
104
105 /**
106 * @throws CommonMarkException
107 */
108 #[DataProvider('providerMalformedLists')]
109 public function testNestedRejectsMalformedLists(string $markdown, string $message): void
110 {
111 $this->expectException(InvalidInputException::class);
112 $this->expectExceptionMessage($message);
113 (new MetadataReader())->nested(MarkdownNodes::first($markdown));
114 }
115
116 /**
117 * @return array<string, array{string, string}>
118 */
119 public static function providerMalformedLists(): array
120 {
121 return [
122 'ordered list' => ['1. a', 'Expected a bullet list.'],
123 'quotation item' => ['- > a', 'Expected a metadata key or value.'],
124 'key after value' => ["- a\n- **b:** 1\n", 'Do not mix mapping keys and sequence values in one metadata list.'],
125 'value after key' => ["- **a:** 1\n- b\n", 'Do not mix mapping keys and sequence values in one metadata list.'],
126 'nested list under a key with a value' => ["- **a:** 1\n - b\n", 'Nested metadata must have an empty parent value and one nested list.'],
127 'nested list under a value' => ["- x\n - b\n", 'Nested metadata must have an empty parent value and one nested list.'],
128 'nested list under an empty list key' => ["- **a:** []\n - b\n", 'Nested metadata must have an empty parent value and one nested list.'],
129 'two nested blocks' => ["- **a:**\n\n - b\n\n c\n", 'Nested metadata must have an empty parent value and one nested list.'],
130 'empty key' => ['- **:** 1', 'Metadata keys must be nonempty and unique.'],
131 'duplicate key' => ["- **a:** 1\n- **a:** 2\n", 'Metadata keys must be nonempty and unique.'],
132 'empty value' => ['- **a:**', 'Write "" for an empty string or supply a metadata value.'],
133 'key without colon' => ['- **a** 1', 'Expected a colon after the bold field name.'],
134 ];
135 }
136
137 #[DataProvider('providerScalars')]
138 public function testScalarDecodesJsonOrKeepsText(string $text, mixed $expected): void
139 {
140 self::assertEquals($expected, (new MetadataReader())->scalar($text));
141 }
142
143 /**
144 * @return array<string, array{string, mixed}>
145 */
146 public static function providerScalars(): array
147 {
148 return [
149 'text' => ['Parser team', 'Parser team'],
150 'JSON string' => ['"true"', 'true'],
151 'empty JSON string' => ['""', ''],
152 'boolean' => ['true', true],
153 'integer' => ['2', 2],
154 'float' => ['1.5', 1.5],
155 'null' => ['null', null],
156 'empty list' => ['[]', []],
157 'object' => ['{"a": 1}', (object) ['a' => 1]],
158 'broken JSON' => ['{"a": 1', '{"a": 1'],
159 ];
160 }
161
162 public function testScalarRejectsAnEmptyValue(): void
163 {
164 $this->expectException(InvalidInputException::class);
165 $this->expectExceptionMessage('Write "" for an empty string or supply a metadata value.');
166 (new MetadataReader())->scalar('');
167 }
168}
169