packages/sql-catalog/tests/Unit/Reporter/Html/CatalogStatisticsTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Reporter\Html;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlCatalog\Core\Catalog\CallSite;
11use SqlCatalog\Core\Catalog\Catalog;
12use SqlCatalog\Core\Catalog\CatalogEntry;
13use SqlCatalog\Core\Catalog\Finding;
14use SqlCatalog\Core\Catalog\FindingRule;
15use SqlCatalog\Core\Catalog\Resolution;
16use SqlCatalog\Core\Catalog\Severity;
17use SqlCatalog\Core\Sql\StatementKind;
18use SqlCatalog\Core\Text\Origin;
19use SqlCatalog\Core\Text\TextHole;
20use SqlCatalog\Core\Text\TextPattern;
21use SqlCatalog\Core\Type\TypeShape;
22use SqlCatalog\Reporter\Html\CatalogStatistics;
23
24#[CoversClass(CatalogStatistics::class)]
25#[UsesClass(CallSite::class)]
26#[UsesClass(Catalog::class)]
27#[UsesClass(CatalogEntry::class)]
28#[UsesClass(Finding::class)]
29#[UsesClass(FindingRule::class)]
30#[UsesClass(Resolution::class)]
31#[UsesClass(Severity::class)]
32#[UsesClass(Origin::class)]
33#[UsesClass(TextHole::class)]
34#[UsesClass(TextPattern::class)]
35#[UsesClass(TypeShape::class)]
36#[UsesClass(StatementKind::class)]
37#[UsesClass(\SqlCatalog\Core\Text\LiteralText::class)]
38final class CatalogStatisticsTest extends TestCase
39{
40    public function testStatementsCountsWhatTheCatalogHolds(): void
41    {
42        $catalog = new Catalog([
43            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), ['t'], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
44        ]);
45
46        self::assertSame(1, (new CatalogStatistics($catalog))->statements());
47    }
48
49    public function testResolvedCountsTheStatementsWithNoGaps(): void
50    {
51        $catalog = new Catalog([
52            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
53            new CatalogEntry('b', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::External, TypeShape::unknown())), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
54        ]);
55
56        self::assertSame(1, (new CatalogStatistics($catalog))->resolved());
57    }
58
59    public function testOpenCountsTheStatementsWhoseSearchDidNotClose(): void
60    {
61        $catalog = new Catalog([
62            new CatalogEntry('a', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::External, TypeShape::unknown())), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
63            new CatalogEntry('b', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::Budget, TypeShape::unknown())), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
64        ]);
65
66        self::assertSame(1, (new CatalogStatistics($catalog))->open());
67    }
68
69    public function testFindingsCountsEveryFindingOfEveryStatement(): void
70    {
71        $catalog = new Catalog([
72            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
73                Finding::of(FindingRule::DynamicSql, 'one'),
74                Finding::of(FindingRule::ExternalInput, 'two'),
75            ]),
76        ]);
77
78        self::assertSame(2, (new CatalogStatistics($catalog))->findings());
79    }
80
81    public function testAtLeastCountsTheStatementsWorthLookingAt(): void
82    {
83        $catalog = new Catalog([
84            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
85                Finding::of(FindingRule::ExternalInput, 'one'),
86            ]),
87            new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), [
88                Finding::of(FindingRule::AnalysisIncomplete, 'two'),
89            ]),
90        ]);
91
92        self::assertSame(1, (new CatalogStatistics($catalog))->atLeast(Severity::High));
93    }
94
95    public function testByResolutionAccountsForEveryStatementUnderEveryResolution(): void
96    {
97        $catalog = new Catalog([
98            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
99        ]);
100
101        self::assertSame(
102            ['resolved' => 1, 'external-input' => 0, 'incomplete-model' => 0, 'incomplete' => 0, 'not-analyzed' => 0],
103            (new CatalogStatistics($catalog))->byResolution(),
104        );
105    }
106
107    public function testByKindCountsTheKindsMostFirst(): void
108    {
109        $catalog = new Catalog([
110            new CatalogEntry('a', StatementKind::Insert, TextPattern::fromText('INSERT INTO t VALUES (1)'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
111            new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
112            new CatalogEntry('c', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 3, 'f', 'pdo.query'), []),
113        ]);
114
115        self::assertSame(['select' => 2, 'insert' => 1], (new CatalogStatistics($catalog))->byKind());
116    }
117
118    public function testByRuleCountsTheFindingsMostFirst(): void
119    {
120        $catalog = new Catalog([
121            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
122                Finding::of(FindingRule::DynamicSql, 'one'),
123                Finding::of(FindingRule::ExternalInput, 'two'),
124            ]),
125            new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), [
126                Finding::of(FindingRule::DynamicSql, 'three'),
127            ]),
128        ]);
129
130        self::assertSame(['dynamic-sql' => 2, 'external-input' => 1], (new CatalogStatistics($catalog))->byRule());
131    }
132
133    public function testTablesSayHowEachOneIsUsed(): void
134    {
135        $catalog = new Catalog([
136            new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), ['posts'], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
137            new CatalogEntry('b', StatementKind::Insert, TextPattern::fromText('INSERT'), ['posts'], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
138            new CatalogEntry('c', StatementKind::Select, TextPattern::fromText('SELECT 2'), ['users'], [], new CallSite('a.php', 3, 'f', 'pdo.query'), []),
139        ]);
140
141        self::assertSame(
142            [
143                ['name' => 'posts', 'reads' => 1, 'writes' => 1, 'statements' => 2],
144                ['name' => 'users', 'reads' => 1, 'writes' => 0, 'statements' => 1],
145            ],
146            (new CatalogStatistics($catalog))->tables(),
147        );
148    }
149
150    public function testFilesSayWhatWasFoundInEachOfThem(): void
151    {
152        $catalog = new Catalog([
153            new CatalogEntry('a', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::Budget, TypeShape::unknown())), [], [], new CallSite('b.php', 1, 'f', 'pdo.query'), [
154                Finding::of(FindingRule::AnalysisIncomplete, 'one'),
155            ]),
156            new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
157        ]);
158
159        self::assertSame(
160            [
161                'a.php' => ['statements' => 1, 'open' => 0, 'findings' => 0],
162                'b.php' => ['statements' => 1, 'open' => 1, 'findings' => 1],
163            ],
164            (new CatalogStatistics($catalog))->files(),
165        );
166    }
167
168    public function testFlaggedListsTheReportedStatementsWorstFirst(): void
169    {
170        $catalog = new Catalog([
171            new CatalogEntry('low', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
172                Finding::of(FindingRule::AnalysisIncomplete, 'one'),
173            ]),
174            new CatalogEntry('high', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), [
175                Finding::of(FindingRule::ExternalInput, 'two'),
176            ]),
177            new CatalogEntry('none', StatementKind::Select, TextPattern::fromText('SELECT 3'), [], [], new CallSite('a.php', 3, 'f', 'pdo.query'), []),
178        ]);
179
180        self::assertSame(['high', 'low'], array_column((new CatalogStatistics($catalog))->flagged(), 'id'));
181    }
182}
183