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