Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Completed
Push — master ( 0656bd...6aded6 )
by Dan
25s queued 18s
created

test_getInstance_always_returns_new_instance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
rs 10
1
<?php declare(strict_types=1);
2
3
namespace SmrTest\lib\DefaultGame;
4
5
use MySqlDatabase;
6
use mysqli;
7
use PHPUnit\Framework\TestCase;
8
use Smr\Container\DiContainer;
9
use Smr\MySqlProperties;
10
11
/**
12
 * Class MySqlDatabaseIntegrationTest
13
 * This is an integration test, but does not need to extend BaseIntegrationTest since we are not writing any data.
14
 * @covers MySqlDatabase
15
 * @package SmrTest\lib\DefaultGame
16
 */
17
class MySqlDatabaseIntegrationTest extends TestCase {
18
19
	protected function setUp(): void {
20
		// Start each test with a fresh container (and mysqli connection).
21
		// This ensures the independence of each test.
22
		DiContainer::initializeContainer();
23
	}
24
25
	public function test_mysql_factory() {
26
		// Given mysql properties are retrieved from the container
27
		$mysqlProperties = DiContainer::get(MySqlProperties::class);
28
		// When using the factory to retrieve a mysqli instance
29
		$mysqlDatabase = MySqlDatabase::mysqliFactory($mysqlProperties);
30
		// Then the connection is successful
31
		self::assertNotNull($mysqlDatabase->server_info);
32
	}
33
34
	public function test__construct_happy_path() {
35
		$mysqlDatabase = DiContainer::get(MySqlDatabase::class);
36
		$this->assertNotNull($mysqlDatabase);
37
	}
38
39
	public function test_getInstance_always_returns_new_instance() {
40
		// Given a MySqlDatabase object
41
		$original = MySqlDatabase::getInstance();
42
		// When calling getInstance again
43
		$second = MySqlDatabase::getInstance();
44
		self::assertNotSame($second, $original);
45
	}
46
47
	public function test_performing_operations_on_closed_database_throws_error() {
48
		// Expectations
49
		$this->expectException(\Error::class);
50
		$this->expectExceptionMessage('Typed property MySqlDatabase::$dbConn must not be accessed before initialization');
51
		// Given a mysql database instance
52
		$mysqlDatabase = MySqlDatabase::getInstance();
53
		// And disconnect is called
54
		$mysqlDatabase->close();
55
		// When calling database methods
56
		$mysqlDatabase->query("foo query");
57
	}
58
59
	public function test_getInstance_will_perform_reconnect_after_connection_closed() {
60
		// Given an original mysql connection
61
		$originalMysql = DiContainer::get(mysqli::class);
62
		// And a mysql database instance
63
		$mysqlDatabase = MySqlDatabase::getInstance();
64
		// And disconnect is called
65
		$mysqlDatabase->close();
66
		// And mysql database is retrieved from the container
67
		$mysqlDatabase = MySqlDatabase::getInstance();
68
		// When performing a query
69
		$mysqlDatabase->query("select 1");
70
		// Then new mysqli instance is not the same as the initial mock
71
		self::assertNotSame($originalMysql, DiContainer::get(mysqli::class));
72
	}
73
74
	public function test_getInstance_will_not_perform_reconnect_if_connection_not_closed() {
75
		// Given an original mysql connection
76
		$originalMysql = DiContainer::get(mysqli::class);
77
		// And a mysql database instance
78
		MySqlDatabase::getInstance();
79
		// And get instance is called again
80
		MySqlDatabase::getInstance();
81
		// Then the two mysqli instances are the same
82
		self::assertSame($originalMysql, DiContainer::get(mysqli::class));
83
	}
84
85
	public function test_escapeMicrotime() {
86
		$db = MySqlDatabase::getInstance();
87
		// The current microtime must not throw an exception
88
		$db->escapeMicrotime(microtime(true));
0 ignored issues
show
Bug introduced by
It seems like microtime(true) can also be of type string; however, parameter $microtime of MySqlDatabase::escapeMicrotime() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
		$db->escapeMicrotime(/** @scrutinizer ignore-type */ microtime(true));
Loading history...
89
		// Check that the formatting preserves all digits
90
		self::assertSame("1608455259123456", $db->escapeMicrotime(1608455259.123456));
91
	}
92
93
	public function test_escapeBoolean() {
94
		$db = MySqlDatabase::getInstance();
95
		// Test both boolean values
96
		self::assertSame("'TRUE'", $db->escapeBoolean(true));
97
		self::assertSame("'FALSE'", $db->escapeBoolean(false));
98
	}
99
100
	public function test_escapeString() {
101
		$db = MySqlDatabase::getInstance();
102
		// Test the empty string
103
		self::assertSame("''", $db->escapeString(''));
104
		self::assertSame('NULL', $db->escapeString('', true)); // nullable
105
		// Test null
106
		self::assertSame('NULL', $db->escapeString(null, true)); // nullable
107
		// Test a normal string
108
		self::assertSame("'bla'", $db->escapeString('bla'));
109
		self::assertSame("'bla'", $db->escapeString('bla', true)); // nullable
110
	}
111
112
	public function test_escapeString_null_throws() {
113
		$db = MySqlDatabase::getInstance();
114
		$this->expectException(\TypeError::class);
115
		$db->escapeString(null);
116
	}
117
118
	public function test_escapeArray() {
119
		$db = MySqlDatabase::getInstance();
120
		// Test a mixed array
121
		self::assertSame("'a',2,'c'", $db->escapeArray(['a', 2, 'c']));
122
		// Test a different implodeString
123
		self::assertSame("'a':2:'c'", $db->escapeArray(['a', 2, 'c'], ':'));
124
		// Test escapeIndividually=false
125
		self::assertSame("'a,2,c'", $db->escapeArray(['a', 2, 'c'], ',', false));
126
		// Test nested arrays
127
		// Warning: The array is flattened, which may be unexpected!
128
		self::assertSame("'a','x',9,2", $db->escapeArray(['a', ['x', 9], 2], ',', true));
129
	}
130
131
	public function test_escapeArray_nested_array_throws() {
132
		// Warning: It is dangerous to use nested arrays with escapeIndividually=false
133
		$db = MySqlDatabase::getInstance();
134
		$this->expectWarning();
135
		$this->expectWarningMessage('Array to string conversion');
136
		$db->escapeArray(['a', ['x', 9, 'y'], 2, 'c'], ':', false);
137
	}
138
139
	public function test_escapeNumber() {
140
		// No escaping is done of numeric types
141
		$db = MySqlDatabase::getInstance();
142
		// Test int
143
		self::assertSame(42, $db->escapeNumber(42));
144
		// Test float
145
		self::assertSame(0.21, $db->escapeNumber(0.21));
146
		// Test numeric string
147
		self::assertSame('42', $db->escapeNumber('42'));
148
	}
149
150
	public function test_escapeNumber_nonnumeric_throws() {
151
		$db = MySqlDatabase::getInstance();
152
		$this->expectException(\RuntimeException::class);
153
		$this->expectExceptionMessage('Not a number');
154
		$db->escapeNumber('bla');
155
	}
156
157
	public function test_escapeObject() {
158
		$db = MySqlDatabase::getInstance();
159
		// Test null
160
		self::assertSame('NULL', $db->escapeObject(null, false, true));
161
		// Test empty array
162
		self::assertSame("'a:0:{}'", $db->escapeObject([]));
163
		// Test empty string
164
		self::assertSame('\'s:0:\"\";\'', $db->escapeObject(''));
165
		// Test zero
166
		self::assertSame("'i:0;'", $db->escapeObject(0));
167
	}
168
169
	public function test_lockTable_throws_if_read_other_table() {
170
		$db = MySqlDatabase::getInstance();
171
		$db->lockTable('player');
172
		$this->expectException(\RuntimeException::class);
173
		$this->expectExceptionMessage("Table 'account' was not locked with LOCK TABLES");
174
		try {
175
			$db->query('SELECT 1 FROM account LIMIT 1');
176
		} catch (\RuntimeException $err) {
177
			// Avoid leaving database in a locked state
178
			$db->unlock();
179
			throw $err;
180
		}
181
	}
182
183
	public function test_lockTable_allows_read() {
184
		$db = MySqlDatabase::getInstance();
185
		$db->lockTable('ship_class');
186
187
		// Perform a query on the locked table
188
		$db->query('SELECT ship_class_name FROM ship_class WHERE ship_class_id = 1');
189
		$db->requireRecord();
190
		self::assertSame($db->getRow(), ['ship_class_name' => 'Hunter']);
191
192
		// After unlock we can access other tables again
193
		$db->unlock();
194
		$db->query('SELECT 1 FROM account LIMIT 1');
195
	}
196
197
	public function test_requireRecord() {
198
		$db = MySqlDatabase::getInstance();
199
		// Create a query that returns one record
200
		$db->query('SELECT 1');
201
		$db->requireRecord();
202
		self::assertSame($db->getRow(), [1 => '1']);
203
	}
204
205
	public function test_requireRecord_too_many_rows() {
206
		$db = MySqlDatabase::getInstance();
207
		// Create a query that returns two rows
208
		$db->query('SELECT 1 UNION SELECT 2');
209
		$this->expectException(\RuntimeException::class);
210
		$this->expectExceptionMessage('One record required, but found 2');
211
		$db->requireRecord();
212
	}
213
214
	public function test_nextRecord_no_resource() {
215
		$db = MySqlDatabase::getInstance();
216
		$this->expectException(\RuntimeException::class);
217
		$this->expectExceptionMessage('No resource to get record from.');
218
		// Call nextRecord before any query is made
219
		$db->nextRecord();
220
	}
221
222
	public function test_nextRecord_no_result() {
223
		$db = MySqlDatabase::getInstance();
224
		// Construct a query that has an empty result set
225
		$db->query('SELECT 1 FROM ship_class WHERE ship_class_id = 0');
226
		self::assertFalse($db->nextRecord());
227
	}
228
229
	public function test_hasField() {
230
		$db = MySqlDatabase::getInstance();
231
		// Construct a query that has the field 'val', but not 'bla'
232
		$db->query('SELECT 1 AS val');
233
		$db->requireRecord();
234
		self::assertTrue($db->hasField('val'));
235
		self::assertFalse($db->hasField('bla'));
236
	}
237
238
	public function test_inversion_of_escape_and_get() {
239
		$db = MySqlDatabase::getInstance();
240
		// [value, escape function, getter, comparator, extra args]
241
		$params = [
242
			[true, 'escapeBoolean', 'getBoolean', 'assertSame', []],
243
			[false, 'escapeBoolean', 'getBoolean', 'assertSame', []],
244
			[3, 'escapeNumber', 'getInt', 'assertSame', []],
245
			[3.14, 'escapeNumber', 'getFloat', 'assertSame', []],
246
			['hello', 'escapeString', 'getField', 'assertSame', []],
247
			// Test nullable objects
248
			[null, 'escapeObject', 'getObject', 'assertSame', [false, true]],
249
			// Test object with compression
250
			[[1, 2, 3], 'escapeObject', 'getObject', 'assertSame', [true]],
251
			// Test object without compression
252
			[[1, 2, 3], 'escapeObject', 'getObject', 'assertSame', []],
253
			// Microtime takes a float and returns a string because of DateTime::createFromFormat
254
			[microtime(true), 'escapeMicrotime', 'getMicrotime', 'assertEquals', []],
255
		];
256
		foreach ($params as list($value, $escaper, $getter, $cmp, $args)) {
257
			$db->query('SELECT ' . $db->$escaper($value, ...$args) . ' AS val');
258
			$db->requireRecord();
259
			self::$cmp($db->$getter('val', ...$args), $value);
260
		}
261
	}
262
263
	public function test_getBoolean_with_non_boolean_field() {
264
		$db = MySqlDatabase::getInstance();
265
		$db->query('SELECT \'bla\'');
266
		$db->requireRecord();
267
		$this->expectException(\RuntimeException::class);
268
		$this->expectExceptionMessage('Field is not a boolean: bla');
269
		$db->getBoolean('bla');
270
	}
271
272
}
273