packages/requirements/tests/Unit/Source/JsonSourceTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Source;
6
7use JsonException;
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\Model\Source;
14use Requirements\Source\JsonPath;
15use Requirements\Source\JsonSource;
16use Requirements\Source\LocalFile;
17use Requirements\Source\ResourceLoader;
18use Requirements\Source\ResourceLocation;
19use Requirements\Source\Unit;
20use RuntimeException;
21use Tests\Fake\ProjectDirectory;
22use Tests\Fake\SourceDocuments;
23
24#[CoversClass(JsonSource::class)]
25#[UsesClass(JsonPath::class)]
26#[UsesClass(LocalFile::class)]
27#[UsesClass(ResourceLoader::class)]
28#[UsesClass(ResourceLocation::class)]
29#[UsesClass(Source::class)]
30#[UsesClass(Unit::class)]
31#[Small]
32final class JsonSourceTest extends TestCase
33{
34    /**
35     * @throws JsonException
36     */
37    #[DataProvider('providerSelectStableDistinctUnits')]
38    public function testSelectStableDistinctUnits(string $selector, int $count): void
39    {
40        $project = new ProjectDirectory();
41        $project->put('source.json', SourceDocuments::JSON);
42        $source = new Source('test', 'source.json', 'json', $selector);
43        $extension = new JsonSource();
44        $units = $extension->select($source, $selector, $project->directory, false);
45
46        self::assertCount($count, $units);
47        self::assertCount($count, array_unique(array_map(static fn (Unit $unit): string => $unit->location, $units)));
48        self::assertSame($units[0]->location, $extension->select($source, $selector, $project->directory, false)[0]->location);
49    }
50
51    /**
52     * @return array<string, array{string, int}>
53     */
54    public static function providerSelectStableDistinctUnits(): array
55    {
56        return [
57            'wildcard' => ['$.rules[*].text', 3],
58            'recursive' => ['$..text', 3],
59            'quoted keys' => ["$['rules'][1]['text']", 1],
60        ];
61    }
62
63    /**
64     * @param list<Unit> $expected
65     * @throws JsonException
66     */
67    #[DataProvider('providerSelectUnits')]
68    public function testSelectLocatesUnitsByJsonPointer(string $document, string $selector, array $expected): void
69    {
70        $project = new ProjectDirectory();
71        $project->put('source.json', $document);
72
73        self::assertEquals($expected, (new JsonSource())->select(new Source('test', 'source.json', 'json', $selector), $selector, $project->directory, false));
74    }
75
76    /**
77     * @return array<string, array{string, string, list<Unit>}>
78     */
79    public static function providerSelectUnits(): array
80    {
81        return [
82            'strings' => [SourceDocuments::JSON, '$.rules[*].text', [
83                new Unit('json:/rules/0/text', 'First rule.'),
84                new Unit('json:/rules/1/text', 'First rule.'),
85                new Unit('json:/rules/2/text', 'Third rule.'),
86            ]],
87            'each path once' => [SourceDocuments::JSON, '$..*..text', [
88                new Unit('json:/rules/0/text', 'First rule.'),
89                new Unit('json:/rules/1/text', 'First rule.'),
90                new Unit('json:/rules/2/text', 'Third rule.'),
91            ]],
92            'normalized string' => ['{"text":"  First\n\trule. "}', '$.text', [new Unit('json:/text', 'First rule.')]],
93            'number' => ['{"n":1.5}', '$.n', [new Unit('json:/n', '1.5')]],
94            'boolean' => ['{"b":true}', '$.b', [new Unit('json:/b', 'true')]],
95            'null' => ['{"z":null}', '$.z', [new Unit('json:/z', 'null')]],
96            'object' => ['{"o":{"url":"a/b","name":"日本"}}', '$.o', [new Unit('json:/o', '{"url":"a/b","name":"日本"}')]],
97            'empty object' => ['{"o":{}}', '$.o', [new Unit('json:/o', '{}')]],
98            'array' => ['{"a":[1,"x"]}', '$.a', [new Unit('json:/a', '[1,"x"]')]],
99            'nothing selected' => [SourceDocuments::JSON, '$.missing', []],
100        ];
101    }
102
103    /**
104     * @throws JsonException
105     */
106    public function testSelectRejectsUnsupportedJsonPathInsteadOfSilentlySelecting(): void
107    {
108        $project = new ProjectDirectory();
109        $project->put('source.json', SourceDocuments::JSON);
110        $source = new Source('test', 'source.json', 'json', '$.rules[?(@.text)]');
111
112        $this->expectException(RuntimeException::class);
113        (new JsonSource())->select($source, $source->selector, $project->directory, false);
114    }
115
116    /**
117     * @throws JsonException
118     */
119    public function testSelectRejectsInvalidJson(): void
120    {
121        $project = new ProjectDirectory();
122        $project->put('source.json', '{"rules":');
123
124        $this->expectException(JsonException::class);
125        (new JsonSource())->select(new Source('test', 'source.json', 'json', '$'), '$', $project->directory, false);
126    }
127
128    /**
129     * @throws JsonException
130     */
131    public function testSelectAcceptsTheNestingLimit(): void
132    {
133        $project = new ProjectDirectory();
134        $project->put('deep.json', str_repeat('[', 511) . str_repeat(']', 511));
135
136        self::assertCount(1, (new JsonSource())->select(new Source('test', 'deep.json', 'json', '$[0]'), '$[0]', $project->directory, false));
137    }
138
139    /**
140     * @throws JsonException
141     */
142    public function testSelectRejectsDocumentsNestedBeyondTheLimit(): void
143    {
144        $project = new ProjectDirectory();
145        $project->put('deep.json', str_repeat('[', 512) . str_repeat(']', 512));
146
147        $this->expectException(JsonException::class);
148        (new JsonSource())->select(new Source('test', 'deep.json', 'json', '$[0]'), '$[0]', $project->directory, false);
149    }
150
151    /**
152     * @throws JsonException
153     */
154    public function testSelectReadsThroughTheGivenLoader(): void
155    {
156        $project = new ProjectDirectory();
157        $project->put('source.json', SourceDocuments::JSON);
158        $loader = new ResourceLoader();
159        $loader->fetch($project->path('source.json'));
160        $project->put('source.json', '{"other":"Changed."}');
161
162        self::assertEquals([new Unit('json:/other', 'Outside scope.')], (new JsonSource($loader))->select(new Source('test', 'source.json', 'json', '$.other'), '$.other', $project->directory, false));
163    }
164}
165