packages/sql-catalog/tests/Unit/Reporter/Html/StatementRowTest.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\Catalog\StatementPart;
18use SqlCatalog\Core\Sql\StatementKind;
19use SqlCatalog\Core\Text\LiteralText;
20use SqlCatalog\Core\Text\Origin;
21use SqlCatalog\Core\Text\TextHole;
22use SqlCatalog\Core\Text\TextPattern;
23use SqlCatalog\Core\Type\TypeShape;
24use SqlCatalog\Reporter\Html\CatalogIndex;
25use SqlCatalog\Reporter\Html\CatalogStatistics;
26use SqlCatalog\Reporter\Html\HtmlText;
27use SqlCatalog\Reporter\Html\Palette;
28use SqlCatalog\Reporter\Html\ReportSite;
29use SqlCatalog\Reporter\Html\Scope;
30use SqlCatalog\Reporter\Html\SqlFormatter;
31use SqlCatalog\Reporter\Html\SqlHighlighter;
32use SqlCatalog\Reporter\Html\StatementRow;
33
34#[CoversClass(StatementRow::class)]
35#[UsesClass(CallSite::class)]
36#[UsesClass(Catalog::class)]
37#[UsesClass(CatalogEntry::class)]
38#[UsesClass(CatalogIndex::class)]
39#[UsesClass(CatalogStatistics::class)]
40#[UsesClass(Finding::class)]
41#[UsesClass(FindingRule::class)]
42#[UsesClass(HtmlText::class)]
43#[UsesClass(Palette::class)]
44#[UsesClass(ReportSite::class)]
45#[UsesClass(Resolution::class)]
46#[UsesClass(Scope::class)]
47#[UsesClass(Severity::class)]
48#[UsesClass(SqlHighlighter::class)]
49#[UsesClass(SqlFormatter::class)]
50#[UsesClass(StatementKind::class)]
51#[UsesClass(StatementPart::class)]
52#[UsesClass(LiteralText::class)]
53#[UsesClass(Origin::class)]
54#[UsesClass(TextHole::class)]
55#[UsesClass(TextPattern::class)]
56#[UsesClass(TypeShape::class)]
57final class StatementRowTest extends TestCase
58{
59    public function testRenderLinksTheStatementAndWhereItIsIssuedRelativeToThePage(): void
60    {
61        $entry = new CatalogEntry('a1', StatementKind::Select, TextPattern::fromText('SELECT 1 FROM users'), ['users'], [], new CallSite('src/a.php', 4, 'App\\R::find', 'pdo.query'), []);
62        $site = new ReportSite(new Catalog([$entry]), formatter: \SqlCatalog\Facade\Builtins::sqlFormatter());
63
64        self::assertSame(
65            '<li class="row" data-kind="select" data-resolution="resolved" data-severity="" data-rule="" data-sink="pdo.query" data-open="" data-table="u'
66                . 'sers" data-namespace="App" data-class="App\\R" data-function="App\\R::find" data-file="src/a.php"><a class="row-main" href="../statements/a1.h'
67                . 'tml"><span class="chip tone-blue">SELECT</span><pre class="code"><span class="tok-kw">SELECT</span>
68    <span class="tok-num">1</span>
69<span'
70                . ' class="tok-kw">FROM</span>
71    users</pre></a><p class="row-meta"><a href="../files/src-a-php.html">src/a.php:4</a><a href="../classes/app-'
72                . 'r.html#fn-app-r-find">R::find</a><a class="chip chip-ghost" href="../tables/users.html">users</a></p></li>',
73            (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->render($site, 'tables/users.html', $entry),
74        );
75    }
76
77    public function testAttributesCarryEveryFactAListingIsNarrowedBy(): void
78    {
79        $entry = new CatalogEntry('a1', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::Budget, TypeShape::unknown())), ['a', 'b'], [], new CallSite('a.php', 4, 'f', 'pdo.query'), [
80            Finding::of(FindingRule::DynamicSql, 'x'),
81            Finding::of(FindingRule::AnalysisIncomplete, 'y'),
82        ]);
83
84        self::assertSame(
85            ' data-kind="select" data-resolution="incomplete" data-severity="medium" data-rule="dynamic-sql analysis-incomplete" data-sink="pdo.query" data-open="o'
86                . 'pen" data-table="a b" data-namespace="" data-class="" data-function="f" data-file="a.php"',
87            (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->attributes($entry),
88        );
89    }
90
91    public function testSqlDoesNotDressUpACallNoStatementWasReadFrom(): void
92    {
93        $entry = new CatalogEntry('a1', StatementKind::Unknown, TextPattern::fromHole(new TextHole(Origin::Unreached, TypeShape::unknown(), '$db->query($sql)')), [], [], new CallSite('a.php', 4, 'f', 'pdo.query'), []);
94
95        self::assertSame(
96            '<span class="tok-com">no statement was read from this call</span> $db-&gt;query($sql)',
97            (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->sql($entry),
98        );
99    }
100
101    public function testMetaOmitsWhatTheListingAlreadyStatesAndShowsWhatIsNotOrdinary(): void
102    {
103        $entry = new CatalogEntry('a1', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::External, TypeShape::unknown())), ['users'], [], new CallSite('a.php', 4, '{main}', 'pdo.query'), [
104            Finding::of(FindingRule::ExternalInput, 'x'),
105        ]);
106        $site = new ReportSite(new Catalog([$entry]), formatter: \SqlCatalog\Facade\Builtins::sqlFormatter());
107
108        self::assertSame(
109            '<span class="chip tone-danger" title="The values were followed to runtime input, so the text cannot be fixed.">external-input</span><span class="chip '
110                . 'tone-danger" title="The most serious finding on this statement">high</span>',
111            (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->meta($site, '', $entry, ['file', 'function', 'tables']),
112        );
113    }
114
115    public function testAttributesNameARuleOnceHoweverOftenItWasReported(): void
116    {
117        $entry = new CatalogEntry('a1', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 4, 'f', 'pdo.query'), [
118            Finding::of(FindingRule::DynamicSql, 'x'),
119            Finding::of(FindingRule::DynamicSql, 'y'),
120        ]);
121
122        self::assertStringContainsString(' data-rule="dynamic-sql" ', (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->attributes($entry));
123    }
124
125    public function testMetaNamesTheFunctionUnlessTheListingAlreadyDoes(): void
126    {
127        $entry = new CatalogEntry('a1', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 4, 'f', 'pdo.query'), [
128            Finding::of(FindingRule::AnalysisIncomplete, 'x'),
129        ]);
130        $site = new ReportSite(new Catalog([$entry]), formatter: \SqlCatalog\Facade\Builtins::sqlFormatter());
131        $row = new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter());
132
133        self::assertSame('<a href="files/a-php.html#fn-f">f</a>', $row->meta($site, '', $entry, ['file', 'tables']));
134        self::assertSame('', $row->meta($site, '', $entry, ['file', 'tables', 'function']));
135    }
136    public function testSqlDisplaysThePhpVariableForAnUnknownStatement(): void
137    {
138        $entry = new CatalogEntry('q', StatementKind::Unknown, TextPattern::fromHole(
139            new TextHole(Origin::Call, TypeShape::unknown(), 'buildSql()', '$sql'),
140        ), [], [], new CallSite('query.php', 4, 'f', 'pdo.prepare'), []);
141
142        self::assertStringEndsWith('>{$sql}</span>', (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->sql($entry));
143        self::assertStringContainsString('Written as buildSql().', (new StatementRow(formatter: \SqlCatalog\Facade\Builtins::sqlFormatter()))->sql($entry));
144    }
145
146}
147