smrealms /
smr
We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.
| 1 | <?php declare(strict_types=1); |
||
| 2 | |||
| 3 | namespace SmrTest\lib\DefaultGame; |
||
| 4 | |||
| 5 | use mysqli; |
||
| 6 | use PHPUnit\Framework\TestCase; |
||
| 7 | use Smr\Container\DiContainer; |
||
| 8 | use Smr\Database; |
||
| 9 | use Smr\DatabaseProperties; |
||
| 10 | |||
| 11 | /** |
||
| 12 | * This is an integration test, but does not need to extend BaseIntegrationTest since we are not writing any data. |
||
| 13 | * @covers \Smr\Database |
||
| 14 | */ |
||
| 15 | class DatabaseIntegrationTest extends TestCase { |
||
| 16 | |||
| 17 | protected function setUp(): void { |
||
| 18 | // Start each test with a fresh container (and mysqli connection). |
||
| 19 | // This ensures the independence of each test. |
||
| 20 | DiContainer::initializeContainer(); |
||
| 21 | } |
||
| 22 | |||
| 23 | public function test_mysqli_factory() { |
||
| 24 | // Given database properties are retrieved from the container |
||
| 25 | $dbProperties = DiContainer::get(DatabaseProperties::class); |
||
| 26 | // When using the factory to retrieve a mysqli instance |
||
| 27 | $mysql = Database::mysqliFactory($dbProperties); |
||
| 28 | // Then the connection is successful |
||
| 29 | self::assertNotNull($mysql->server_info); |
||
| 30 | } |
||
| 31 | |||
| 32 | public function test__construct_happy_path() { |
||
| 33 | $db = DiContainer::get(Database::class); |
||
| 34 | $this->assertNotNull($db); |
||
| 35 | } |
||
| 36 | |||
| 37 | public function test_getInstance_always_returns_new_instance() { |
||
| 38 | // Given a Database object |
||
| 39 | $original = Database::getInstance(); |
||
| 40 | // When calling getInstance again |
||
| 41 | $second = Database::getInstance(); |
||
| 42 | self::assertNotSame($original, $second); |
||
| 43 | } |
||
| 44 | |||
| 45 | public function test_performing_operations_on_closed_database_throws_error() { |
||
| 46 | // Given a Database instance |
||
| 47 | $db = Database::getInstance(); |
||
| 48 | // And disconnect is called |
||
| 49 | $db->close(); |
||
| 50 | // When calling database methods |
||
| 51 | $this->expectException(\Error::class); |
||
| 52 | $this->expectExceptionMessage('Typed property Smr\Database::$dbConn must not be accessed before initialization'); |
||
| 53 | $db->query("foo query"); |
||
| 54 | } |
||
| 55 | |||
| 56 | public function test_closing_database_returns_boolean() { |
||
| 57 | $db = Database::getInstance(); |
||
| 58 | // Returns true when closing an open database connection |
||
| 59 | self::assertTrue($db->close()); |
||
| 60 | // Returns false if the database has already been closed |
||
| 61 | self::assertFalse($db->close()); |
||
| 62 | } |
||
| 63 | |||
| 64 | public function test_getInstance_will_perform_reconnect_after_connection_closed() { |
||
| 65 | // Given an original mysql connection |
||
| 66 | $originalMysql = DiContainer::get(mysqli::class); |
||
| 67 | // And a Database instance |
||
| 68 | $db = Database::getInstance(); |
||
| 69 | // And disconnect is called |
||
| 70 | $db->close(); |
||
| 71 | // And Database is retrieved from the container |
||
| 72 | $db = Database::getInstance(); |
||
| 73 | // When performing a query |
||
| 74 | $db ->query("select 1"); |
||
| 75 | // Then new mysqli instance is not the same as the initial mock |
||
| 76 | self::assertNotSame($originalMysql, DiContainer::get(mysqli::class)); |
||
| 77 | } |
||
| 78 | |||
| 79 | public function test_getInstance_will_not_perform_reconnect_if_connection_not_closed() { |
||
| 80 | // Given an original mysql connection |
||
| 81 | $originalMysql = DiContainer::get(mysqli::class); |
||
| 82 | // And a Database instance |
||
| 83 | Database::getInstance(); |
||
| 84 | // And get instance is called again |
||
| 85 | Database::getInstance(); |
||
| 86 | // Then the two mysqli instances are the same |
||
| 87 | self::assertSame($originalMysql, DiContainer::get(mysqli::class)); |
||
| 88 | } |
||
| 89 | |||
| 90 | public function test_escapeMicrotime() { |
||
| 91 | $db = Database::getInstance(); |
||
| 92 | // The current microtime must not throw an exception |
||
| 93 | $db->escapeMicrotime(microtime(true)); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 94 | // Check that the formatting preserves all digits |
||
| 95 | self::assertSame("1608455259123456", $db->escapeMicrotime(1608455259.123456)); |
||
| 96 | } |
||
| 97 | |||
| 98 | public function test_escapeBoolean() { |
||
| 99 | $db = Database::getInstance(); |
||
| 100 | // Test both boolean values |
||
| 101 | self::assertSame("'TRUE'", $db->escapeBoolean(true)); |
||
| 102 | self::assertSame("'FALSE'", $db->escapeBoolean(false)); |
||
| 103 | } |
||
| 104 | |||
| 105 | public function test_escapeString() { |
||
| 106 | $db = Database::getInstance(); |
||
| 107 | // Test the empty string |
||
| 108 | self::assertSame("''", $db->escapeString('')); |
||
| 109 | self::assertSame('NULL', $db->escapeString('', true)); // nullable |
||
| 110 | // Test null |
||
| 111 | self::assertSame('NULL', $db->escapeString(null, true)); // nullable |
||
| 112 | // Test a normal string |
||
| 113 | self::assertSame("'bla'", $db->escapeString('bla')); |
||
| 114 | self::assertSame("'bla'", $db->escapeString('bla', true)); // nullable |
||
| 115 | } |
||
| 116 | |||
| 117 | public function test_escapeString_null_throws() { |
||
| 118 | $db = Database::getInstance(); |
||
| 119 | $this->expectException(\TypeError::class); |
||
| 120 | $db->escapeString(null); |
||
| 121 | } |
||
| 122 | |||
| 123 | public function test_escapeArray() { |
||
| 124 | $db = Database::getInstance(); |
||
| 125 | // Test a mixed array |
||
| 126 | self::assertSame("'a',2,'c'", $db->escapeArray(['a', 2, 'c'])); |
||
| 127 | // Test a different implodeString |
||
| 128 | self::assertSame("'a':2:'c'", $db->escapeArray(['a', 2, 'c'], ':')); |
||
| 129 | // Test escapeIndividually=false |
||
| 130 | self::assertSame("'a,2,c'", $db->escapeArray(['a', 2, 'c'], ',', false)); |
||
| 131 | // Test nested arrays |
||
| 132 | // Warning: The array is flattened, which may be unexpected! |
||
| 133 | self::assertSame("'a','x',9,2", $db->escapeArray(['a', ['x', 9], 2], ',', true)); |
||
| 134 | } |
||
| 135 | |||
| 136 | public function test_escapeArray_nested_array_throws() { |
||
| 137 | // Warning: It is dangerous to use nested arrays with escapeIndividually=false |
||
| 138 | $db = Database::getInstance(); |
||
| 139 | $this->expectWarning(); |
||
| 140 | $this->expectWarningMessage('Array to string conversion'); |
||
| 141 | $db->escapeArray(['a', ['x', 9, 'y'], 2, 'c'], ':', false); |
||
| 142 | } |
||
| 143 | |||
| 144 | public function test_escapeNumber() { |
||
| 145 | // No escaping is done of numeric types |
||
| 146 | $db = Database::getInstance(); |
||
| 147 | // Test int |
||
| 148 | self::assertSame(42, $db->escapeNumber(42)); |
||
| 149 | // Test float |
||
| 150 | self::assertSame(0.21, $db->escapeNumber(0.21)); |
||
| 151 | // Test numeric string |
||
| 152 | self::assertSame('42', $db->escapeNumber('42')); |
||
| 153 | } |
||
| 154 | |||
| 155 | public function test_escapeNumber_nonnumeric_throws() { |
||
| 156 | $db = Database::getInstance(); |
||
| 157 | $this->expectException(\RuntimeException::class); |
||
| 158 | $this->expectExceptionMessage('Not a number'); |
||
| 159 | $db->escapeNumber('bla'); |
||
| 160 | } |
||
| 161 | |||
| 162 | public function test_escapeObject() { |
||
| 163 | $db = Database::getInstance(); |
||
| 164 | // Test null |
||
| 165 | self::assertSame('NULL', $db->escapeObject(null, false, true)); |
||
| 166 | // Test empty array |
||
| 167 | self::assertSame("'a:0:{}'", $db->escapeObject([])); |
||
| 168 | // Test empty string |
||
| 169 | self::assertSame('\'s:0:\"\";\'', $db->escapeObject('')); |
||
| 170 | // Test zero |
||
| 171 | self::assertSame("'i:0;'", $db->escapeObject(0)); |
||
| 172 | } |
||
| 173 | |||
| 174 | public function test_lockTable_throws_if_read_other_table() { |
||
| 175 | $db = Database::getInstance(); |
||
| 176 | $db->lockTable('player'); |
||
| 177 | $this->expectException(\RuntimeException::class); |
||
| 178 | $this->expectExceptionMessage("Table 'account' was not locked with LOCK TABLES"); |
||
| 179 | try { |
||
| 180 | $db->query('SELECT 1 FROM account LIMIT 1'); |
||
| 181 | } catch (\RuntimeException $err) { |
||
| 182 | // Avoid leaving database in a locked state |
||
| 183 | $db->unlock(); |
||
| 184 | throw $err; |
||
| 185 | } |
||
| 186 | } |
||
| 187 | |||
| 188 | public function test_lockTable_allows_read() { |
||
| 189 | $db = Database::getInstance(); |
||
| 190 | $db->lockTable('ship_class'); |
||
| 191 | |||
| 192 | // Perform a query on the locked table |
||
| 193 | $db->query('SELECT ship_class_name FROM ship_class WHERE ship_class_id = 1'); |
||
| 194 | $db->requireRecord(); |
||
| 195 | self::assertSame(['ship_class_name' => 'Hunter'], $db->getRow()); |
||
| 196 | |||
| 197 | // After unlock we can access other tables again |
||
| 198 | $db->unlock(); |
||
| 199 | $db->query('SELECT 1 FROM account LIMIT 1'); |
||
| 200 | } |
||
| 201 | |||
| 202 | public function test_requireRecord() { |
||
| 203 | $db = Database::getInstance(); |
||
| 204 | // Create a query that returns one record |
||
| 205 | $db->query('SELECT 1'); |
||
| 206 | $db->requireRecord(); |
||
| 207 | self::assertSame([1 => '1'], $db->getRow()); |
||
| 208 | } |
||
| 209 | |||
| 210 | public function test_requireRecord_too_many_rows() { |
||
| 211 | $db = Database::getInstance(); |
||
| 212 | // Create a query that returns two rows |
||
| 213 | $db->query('SELECT 1 UNION SELECT 2'); |
||
| 214 | $this->expectException(\RuntimeException::class); |
||
| 215 | $this->expectExceptionMessage('One record required, but found 2'); |
||
| 216 | $db->requireRecord(); |
||
| 217 | } |
||
| 218 | |||
| 219 | public function test_nextRecord_no_resource() { |
||
| 220 | $db = Database::getInstance(); |
||
| 221 | $this->expectException(\RuntimeException::class); |
||
| 222 | $this->expectExceptionMessage('No resource to get record from.'); |
||
| 223 | // Call nextRecord before any query is made |
||
| 224 | $db->nextRecord(); |
||
| 225 | } |
||
| 226 | |||
| 227 | public function test_nextRecord_no_result() { |
||
| 228 | $db = Database::getInstance(); |
||
| 229 | // Construct a query that has an empty result set |
||
| 230 | $db->query('SELECT 1 FROM ship_class WHERE ship_class_id = 0'); |
||
| 231 | self::assertFalse($db->nextRecord()); |
||
| 232 | } |
||
| 233 | |||
| 234 | public function test_hasField() { |
||
| 235 | $db = Database::getInstance(); |
||
| 236 | // Construct a query that has the field 'val', but not 'bla' |
||
| 237 | $db->query('SELECT 1 AS val'); |
||
| 238 | $db->requireRecord(); |
||
| 239 | self::assertTrue($db->hasField('val')); |
||
| 240 | self::assertFalse($db->hasField('bla')); |
||
| 241 | } |
||
| 242 | |||
| 243 | public function test_inversion_of_escape_and_get() { |
||
| 244 | $db = Database::getInstance(); |
||
| 245 | // [value, escape function, getter, comparator, extra args] |
||
| 246 | $params = [ |
||
| 247 | [true, 'escapeBoolean', 'getBoolean', 'assertSame', []], |
||
| 248 | [false, 'escapeBoolean', 'getBoolean', 'assertSame', []], |
||
| 249 | [3, 'escapeNumber', 'getInt', 'assertSame', []], |
||
| 250 | [3.14, 'escapeNumber', 'getFloat', 'assertSame', []], |
||
| 251 | ['hello', 'escapeString', 'getField', 'assertSame', []], |
||
| 252 | // Test nullable objects |
||
| 253 | [null, 'escapeObject', 'getObject', 'assertSame', [false, true]], |
||
| 254 | // Test object with compression |
||
| 255 | [[1, 2, 3], 'escapeObject', 'getObject', 'assertSame', [true]], |
||
| 256 | // Test object without compression |
||
| 257 | [[1, 2, 3], 'escapeObject', 'getObject', 'assertSame', []], |
||
| 258 | // Microtime takes a float and returns a string because of DateTime::createFromFormat |
||
| 259 | [microtime(true), 'escapeMicrotime', 'getMicrotime', 'assertEquals', []], |
||
| 260 | ]; |
||
| 261 | foreach ($params as list($value, $escaper, $getter, $cmp, $args)) { |
||
| 262 | $db->query('SELECT ' . $db->$escaper($value, ...$args) . ' AS val'); |
||
| 263 | $db->requireRecord(); |
||
| 264 | self::$cmp($value, $db->$getter('val', ...$args)); |
||
| 265 | } |
||
| 266 | } |
||
| 267 | |||
| 268 | public function test_getBoolean_with_non_boolean_field() { |
||
| 269 | $db = Database::getInstance(); |
||
| 270 | $db->query('SELECT \'bla\''); |
||
| 271 | $db->requireRecord(); |
||
| 272 | $this->expectException(\RuntimeException::class); |
||
| 273 | $this->expectExceptionMessage('Field is not a boolean: bla'); |
||
| 274 | $db->getBoolean('bla'); |
||
| 275 | } |
||
| 276 | |||
| 277 | } |
||
| 278 |