1<?php
2
3declare(strict_types=1);
4
5namespace ZnoteX\Tests\Security;
6
7use PHPUnit\Framework\TestCase;
8
9final class MigrationSqlSplitterTest extends TestCase
10{
11 public function testSplitsSimpleStatements(): void
12 {
13 $sql = "CREATE TABLE a (id INT);\nINSERT INTO a VALUES (1);";
14 $this->assertSame(
15 ['CREATE TABLE a (id INT)', 'INSERT INTO a VALUES (1)'],
16 \znote_migration_split_sql($sql)
17 );
18 }
19
20 public function testSemicolonInsideAStringLiteralDoesNotSplit(): void
21 {
22 $sql = "INSERT INTO a (name) VALUES ('it;s here');";
23 $statements = \znote_migration_split_sql($sql);
24 $this->assertCount(1, $statements);
25 $this->assertStringContainsString("'it;s here'", $statements[0]);
26 }
27
28 public function testSemicolonInsideALineCommentDoesNotSplit(): void
29 {
30 // The comment has no terminating semicolon of its own, so it attaches
31 // to the following statement as a single chunk.
32 $sql = "-- comment ; still comment\nINSERT INTO a VALUES (1);";
33 $statements = \znote_migration_split_sql($sql);
34 $this->assertCount(1, $statements);
35 $this->assertStringContainsString('INSERT INTO a VALUES (1)', $statements[0]);
36 }
37
38 public function testSemicolonInsideABlockCommentDoesNotSplit(): void
39 {
40 $sql = "/* a ; block ; comment */ INSERT INTO a VALUES (1);";
41 $statements = \znote_migration_split_sql($sql);
42 $this->assertCount(1, $statements);
43 }
44
45 public function testEscapedQuoteInsideAStringDoesNotCloseIt(): void
46 {
47 $sql = "INSERT INTO a (name) VALUES ('it\\'s a trap; still one statement');";
48 $statements = \znote_migration_split_sql($sql);
49 $this->assertCount(1, $statements);
50 }
51
52 public function testHashStartsALineComment(): void
53 {
54 $sql = "# hash comment ; here\nINSERT INTO a VALUES (2);";
55 $statements = \znote_migration_split_sql($sql);
56 $this->assertCount(1, $statements);
57 $this->assertStringContainsString('INSERT INTO a VALUES (2)', $statements[0]);
58 }
59
60 public function testEmptyStatementsAreDropped(): void
61 {
62 $sql = "INSERT INTO a VALUES (1);;; ;\nINSERT INTO a VALUES (2);";
63 $statements = \znote_migration_split_sql($sql);
64 $this->assertCount(2, $statements);
65 }
66
67 public function testTrailingStatementWithoutASemicolonIsKept(): void
68 {
69 $sql = "INSERT INTO a VALUES (1);\nINSERT INTO a VALUES (2)";
70 $statements = \znote_migration_split_sql($sql);
71 $this->assertCount(2, $statements);
72 $this->assertSame('INSERT INTO a VALUES (2)', $statements[1]);
73 }
74
75 public function testDestructiveStatementsAreRejectedByTheUpdaterSafetyCheck(): void
76 {
77 $this->assertFalse(\znote_update_migration_safe('DROP TABLE accounts;'));
78 $this->assertFalse(\znote_update_migration_safe('DELETE FROM accounts;'));
79 $this->assertFalse(\znote_update_migration_safe('UPDATE accounts SET password = \'\';'));
80 $this->assertFalse(\znote_update_migration_safe('TRUNCATE accounts;'));
81 }
82
83 public function testAdditiveStatementsAreAcceptedByTheUpdaterSafetyCheck(): void
84 {
85 $this->assertTrue(\znote_update_migration_safe('CREATE TABLE IF NOT EXISTS foo (id INT);'));
86 $this->assertTrue(\znote_update_migration_safe('ALTER TABLE foo ADD COLUMN bar INT;'));
87 $this->assertTrue(\znote_update_migration_safe("INSERT IGNORE INTO foo (id) VALUES (1);"));
88 }
89
90 public function testAMixOfSafeAndDestructiveStatementsIsRejected(): void
91 {
92 $sql = "CREATE TABLE IF NOT EXISTS foo (id INT);\nDROP TABLE bar;";
93 $this->assertFalse(\znote_update_migration_safe($sql));
94 }
95
96 public function testDestructiveKeywordHiddenInAStringLiteralIsStillCaughtByTheRegexScan(): void
97 {
98 // znote_update_migration_safe() only strips comments, not string
99 // contents, so a DROP/DELETE keyword anywhere in the statement -
100 // even inside a quoted value - is treated as unsafe. This is a
101 // deliberately conservative false positive, not a bypass.
102 $sql = "INSERT IGNORE INTO foo (note) VALUES ('please DROP by later');";
103 $this->assertFalse(\znote_update_migration_safe($sql));
104 }
105}
106