packages/requirements/tests/Unit/Config/Markdown/Profile/AllowedBlocksTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Config\Markdown\Profile;
6
7use League\CommonMark\Environment\Environment;
8use League\CommonMark\Exception\CommonMarkException;
9use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
10use League\CommonMark\Node\Node;
11use League\CommonMark\Parser\MarkdownParser;
12use PHPUnit\Framework\Attributes\CoversClass;
13use PHPUnit\Framework\Attributes\DataProvider;
14use PHPUnit\Framework\Attributes\Small;
15use PHPUnit\Framework\Attributes\UsesClass;
16use PHPUnit\Framework\TestCase;
17use Requirements\Config\Markdown\Profile\AllowedBlocks;
18use Requirements\Config\Markdown\Profile\BlockKind;
19use Requirements\Config\Markdown\Quotation;
20use Requirements\Input\Fields;
21use Requirements\Input\InvalidInputException;
22
23#[CoversClass(AllowedBlocks::class)]
24#[UsesClass(BlockKind::class)]
25#[UsesClass(Fields::class)]
26#[UsesClass(Quotation::class)]
27#[Small]
28final class AllowedBlocksTest extends TestCase
29{
30 /**
31 * @throws CommonMarkException
32 */
33 #[DataProvider('providerAllowed')]
34 public function testCheckAcceptsAllowedBlocks(string $markdown): void
35 {
36 $environment = new Environment();
37 $environment->addExtension(new CommonMarkCoreExtension());
38 $node = (new MarkdownParser($environment))->parse($markdown)->firstChild();
39 self::assertInstanceOf(Node::class, $node);
40 AllowedBlocks::check($node, ['type' => ['paragraph', 'bullet-list', 'quote']], 'doc.md');
41 $this->addToAssertionCount(1);
42 }
43
44 /**
45 * @return array<string, array{string}>
46 */
47 public static function providerAllowed(): array
48 {
49 return [
50 'paragraph' => ["The reader shall emit a tree.\n"],
51 'bullet list' => ["- one\n- two\n"],
52 'nested bullet list' => ["- one\n - two\n"],
53 'quote' => ["> Quoted.\n"],
54 'quote starting with selector comment' => ["> <!-- selector: #a -->\n> Quoted.\n"],
55 'quote starting with bold selector comment' => ["> <!-- **selector:** #a -->\n> Quoted.\n"],
56 'nested quote starting with selector comment' => ["> > <!-- selector: #a -->\n> > Quoted.\n"],
57 'quote in a list item' => ["- > <!-- selector: #a -->\n > Quoted.\n"],
58 'list in a quote' => ["> - one\n"],
59 ];
60 }
61
62 /**
63 * @throws CommonMarkException
64 */
65 #[DataProvider('providerForbidden')]
66 public function testCheckRejectsForbiddenBlocks(string $markdown, string $kind): void
67 {
68 $environment = new Environment();
69 $environment->addExtension(new CommonMarkCoreExtension());
70 $node = (new MarkdownParser($environment))->parse($markdown)->firstChild();
71 self::assertInstanceOf(Node::class, $node);
72 $this->expectException(InvalidInputException::class);
73 $this->expectExceptionMessage("doc.md: document-schema forbids $kind blocks; use quotations, paragraphs and bullet lists.");
74 AllowedBlocks::check($node, ['type' => ['paragraph', 'bullet-list', 'quote']], 'doc.md');
75 }
76
77 /**
78 * @return array<string, array{string, string}>
79 */
80 public static function providerForbidden(): array
81 {
82 return [
83 'ordered list' => ["1. one\n", 'ordered-list'],
84 'code fence' => ["```\ncode\n```\n", 'unsupported'],
85 'heading' => ["# Title\n", 'unsupported'],
86 'comment outside a quote' => ["<!-- selector: #a -->\n", 'unsupported'],
87 'comment after the start of a quote' => ["> Quoted.\n>\n> <!-- selector: #a -->\n", 'unsupported'],
88 'code fence in a quote' => ["> ```\n> code\n> ```\n", 'unsupported'],
89 'code fence in a list item' => ["- one\n\n ```\n code\n ```\n", 'unsupported'],
90 'ordered list in a list item' => ["- one\n 1. two\n", 'ordered-list'],
91 'code fence in a nested quote' => ["> > ```\n> > code\n> > ```\n", 'unsupported'],
92 ];
93 }
94
95 /**
96 * @throws CommonMarkException
97 */
98 public function testCheckRejectsKindMissingFromSchema(): void
99 {
100 $environment = new Environment();
101 $environment->addExtension(new CommonMarkCoreExtension());
102 $node = (new MarkdownParser($environment))->parse("> Quoted.\n")->firstChild();
103 self::assertInstanceOf(Node::class, $node);
104 $this->expectException(InvalidInputException::class);
105 $this->expectExceptionMessage('doc.md: document-schema forbids quote blocks; use quotations, paragraphs and bullet lists.');
106 AllowedBlocks::check($node, ['type' => ['paragraph']], 'doc.md');
107 }
108
109 /**
110 * @throws CommonMarkException
111 */
112 public function testCheckRejectsNestedKindMissingFromSchema(): void
113 {
114 $environment = new Environment();
115 $environment->addExtension(new CommonMarkCoreExtension());
116 $node = (new MarkdownParser($environment))->parse("> Quoted.\n")->firstChild();
117 self::assertInstanceOf(Node::class, $node);
118 $this->expectException(InvalidInputException::class);
119 $this->expectExceptionMessage('doc.md: document-schema forbids paragraph blocks; use quotations, paragraphs and bullet lists.');
120 AllowedBlocks::check($node, ['type' => ['quote']], 'doc.md');
121 }
122
123 /**
124 * @throws CommonMarkException
125 */
126 public function testCheckRejectsCommentThatIsNotSelector(): void
127 {
128 $environment = new Environment();
129 $environment->addExtension(new CommonMarkCoreExtension());
130 $node = (new MarkdownParser($environment))->parse("> <!-- note -->\n> Quoted.\n")->firstChild();
131 self::assertInstanceOf(Node::class, $node);
132 $this->expectException(InvalidInputException::class);
133 $this->expectExceptionMessage('Only a selector comment is allowed at the start of an evidence quotation.');
134 AllowedBlocks::check($node, ['type' => ['paragraph', 'bullet-list', 'quote']], 'doc.md');
135 }
136
137 /**
138 * @param array<string, mixed> $schema
139 * @throws CommonMarkException
140 */
141 #[DataProvider('providerMalformedSchema')]
142 public function testCheckRejectsMalformedSchema(array $schema, string $message): void
143 {
144 $environment = new Environment();
145 $environment->addExtension(new CommonMarkCoreExtension());
146 $node = (new MarkdownParser($environment))->parse("Text.\n")->firstChild();
147 self::assertInstanceOf(Node::class, $node);
148 $this->expectException(InvalidInputException::class);
149 $this->expectExceptionMessage($message);
150 AllowedBlocks::check($node, $schema, 'doc.md');
151 }
152
153 /**
154 * @return array<string, array{array<string, mixed>, string}>
155 */
156 public static function providerMalformedSchema(): array
157 {
158 return [
159 'unknown key' => [['type' => ['paragraph'], 'items' => []], "allBlocks: unknown field 'items'."],
160 'type not a list' => [['type' => 'paragraph'], 'allBlocks.type must be a list.'],
161 ];
162 }
163}
164