packages/requirements/tests/Unit/Config/Markdown/Profile/TextConstraintTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Config\Markdown\Profile;
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\Profile\TextConstraint;
13use Requirements\Input\Fields;
14use Requirements\Input\InvalidInputException;
15
16#[CoversClass(TextConstraint::class)]
17#[UsesClass(Fields::class)]
18#[Small]
19final class TextConstraintTest extends TestCase
20{
21    /**
22     * @param array<string, mixed> $schema
23     */
24    #[DataProvider('providerTexts')]
25    public function testMatchesEvaluatesConstraints(string $text, array $schema, bool $expected): void
26    {
27        self::assertSame($expected, TextConstraint::matches($text, $schema));
28    }
29
30    /**
31     * @return array<string, array{string, array<string, mixed>, bool}>
32     */
33    public static function providerTexts(): array
34    {
35        return [
36            'no constraint' => ['anything', [], true],
37            'pattern matches' => ['SPEC-001', ['pattern' => '^[A-Za-z][A-Za-z0-9_.-]*$'], true],
38            'pattern does not match' => ['1SPEC', ['pattern' => '^[A-Za-z][A-Za-z0-9_.-]*$'], false],
39            'unanchored pattern matches inside' => ['abc', ['pattern' => 'b'], true],
40            'pattern with tilde' => ['a~b', ['pattern' => '^a~b$'], true],
41            'pattern with tilde does not match' => ['ab', ['pattern' => '^a~b$'], false],
42            'pattern in unicode mode' => ["\u{e9}", ['pattern' => '^.$'], true],
43            'const equal' => ['origin', ['const' => 'origin'], true],
44            'const different' => ['Origin', ['const' => 'origin'], false],
45            'enum contains' => ['b', ['enum' => ['a', 'b']], true],
46            'enum lacks' => ['c', ['enum' => ['a', 'b']], false],
47            'null const ignored' => ['x', ['const' => null], true],
48            'null enum ignored' => ['x', ['enum' => null], true],
49            'pattern matches but const differs' => ['abc', ['pattern' => 'a', 'const' => 'abd'], false],
50            'pattern matches but enum lacks' => ['abc', ['pattern' => 'a', 'enum' => ['abd']], false],
51            'const matches but enum lacks' => ['abc', ['const' => 'abc', 'enum' => ['abd']], false],
52            'every constraint holds' => ['abc', ['pattern' => '^a', 'const' => 'abc', 'enum' => ['abc']], true],
53        ];
54    }
55
56    /**
57     * @param array<string, mixed> $schema
58     */
59    #[DataProvider('providerMalformed')]
60    public function testMatchesRejectsMalformedConstraint(array $schema, string $message): void
61    {
62        $this->expectException(InvalidInputException::class);
63        $this->expectExceptionMessage($message);
64        TextConstraint::matches('text', $schema);
65    }
66
67    /**
68     * @return array<string, array{array<string, mixed>, string}>
69     */
70    public static function providerMalformed(): array
71    {
72        return [
73            'unknown key' => [['format' => 'id'], "text constraint: unknown field 'format'."],
74            'pattern not a string' => [['pattern' => 1], 'pattern must be a nonempty string.'],
75            'blank pattern' => [['pattern' => ' '], 'pattern must be a nonempty string.'],
76            'enum not a list' => [['enum' => 'a'], 'enum must be a list.'],
77            'enum with non-string' => [['enum' => [1]], 'enum must contain nonempty strings.'],
78        ];
79    }
80}
81