@@ -292,4 +292,151 @@ public function test_connection_exception_is_thrown_for_bad_credentials(): void
292292 $ this ->expectException (ConnectionException::class);
293293 $ connection ->getPDO ();
294294 }
295+
296+ /**
297+ * Regression for BUG-3: when no driver is configured, `connect()` should
298+ * read the actual driver name from the live PDO instance.
299+ */
300+ public function test_connect_reads_driver_name_from_pdo_when_unset (): void
301+ {
302+ $ connection = new Connection ([
303+ 'dsn ' => 'sqlite::memory: ' ,
304+ 'driver ' => '' ,
305+ 'charset ' => '' ,
306+ ]);
307+
308+ $ connection ->getPDO ();
309+ self ::assertSame ('sqlite ' , $ connection ->getDriver ());
310+ }
311+
312+ public function test_trivial_setters_and_getters_round_trip (): void
313+ {
314+ $ connection = SqliteHelper::makeConnection ();
315+
316+ $ connection ->setHost ('h ' )
317+ ->setPort (1234 )
318+ ->setDsn ('sqlite::memory: ' )
319+ ->setUsername ('u ' )
320+ ->setPassword ('p ' )
321+ ->setDriver ('mysql ' )
322+ ->setCharset ('utf8mb4 ' , 'utf8mb4_unicode_ci ' )
323+ ->setDebug (true );
324+
325+ self ::assertSame ('h ' , $ connection ->getHost ());
326+ self ::assertSame (1234 , $ connection ->getPort ());
327+ self ::assertSame ('sqlite::memory: ' , $ connection ->getDsn ());
328+ self ::assertSame ('u ' , $ connection ->getUsername ());
329+ self ::assertSame ('p ' , $ connection ->getPassword ());
330+ self ::assertSame ('mysql ' , $ connection ->getDriver ());
331+ self ::assertSame ('utf8mb4 ' , $ connection ->getCharset ());
332+ self ::assertSame ('utf8mb4_unicode_ci ' , $ connection ->getCollation ());
333+ self ::assertTrue ($ connection ->getDebug ());
334+ }
335+
336+ public function test_setDriver_rejects_unsafe_identifier (): void
337+ {
338+ $ connection = SqliteHelper::makeConnection ();
339+
340+ $ this ->expectException (ConnectionInvalidArgumentException::class);
341+ $ connection ->setDriver ("mysql'; -- " );
342+ }
343+
344+ public function test_setCharset_rejects_unsafe_collation (): void
345+ {
346+ $ connection = SqliteHelper::makeConnection ();
347+
348+ $ this ->expectException (ConnectionInvalidArgumentException::class);
349+ $ connection ->setCharset ('utf8 ' , "utf8'; DROP " );
350+ }
351+
352+ public function test_getDatabase_returns_null_for_empty_string (): void
353+ {
354+ $ connection = SqliteHelper::makeConnection (['database ' => '' ]);
355+
356+ self ::assertNull ($ connection ->getDatabase ());
357+ }
358+
359+ /**
360+ * Run the MySQL-only charset branch by spoofing the credentials driver
361+ * to 'mysql' after the underlying PDO has been opened against SQLite.
362+ * SQLite rejects `SET NAMES`, so the method bubbles a PDOException —
363+ * but every branch up to the `exec()` call is exercised.
364+ */
365+ public function test_applyCharsetAndCollation_mysql_branch_with_collation (): void
366+ {
367+ $ connection = SqliteHelper::makeConnection ([
368+ 'charset ' => 'utf8mb4 ' ,
369+ 'collation ' => 'utf8mb4_unicode_ci ' ,
370+ ]);
371+ $ connection ->getPDO ();
372+ $ this ->spoofDriver ($ connection , 'mysql ' );
373+
374+ $ this ->expectException (\PDOException::class);
375+ $ this ->invokePrivate ($ connection , 'applyCharsetAndCollation ' );
376+ }
377+
378+ public function test_applyCharsetAndCollation_mysql_branch_without_collation (): void
379+ {
380+ $ connection = SqliteHelper::makeConnection ([
381+ 'charset ' => 'utf8mb4 ' ,
382+ 'collation ' => null ,
383+ ]);
384+ $ connection ->getPDO ();
385+ $ this ->spoofDriver ($ connection , 'mysql ' );
386+
387+ $ this ->expectException (\PDOException::class);
388+ $ this ->invokePrivate ($ connection , 'applyCharsetAndCollation ' );
389+ }
390+
391+ public function test_applyCharsetAndCollation_returns_early_for_empty_charset (): void
392+ {
393+ $ connection = SqliteHelper::makeConnection (['charset ' => '' ]);
394+ $ connection ->getPDO ();
395+ $ this ->spoofDriver ($ connection , 'mysql ' );
396+
397+ // No exception — early return path.
398+ $ this ->invokePrivate ($ connection , 'applyCharsetAndCollation ' );
399+ self ::assertTrue (true );
400+ }
401+
402+ public function test_applyCharsetAndCollation_returns_early_for_unsafe_charset (): void
403+ {
404+ $ connection = SqliteHelper::makeConnection (['charset ' => 'utf8 ' ]);
405+ $ connection ->getPDO ();
406+ $ this ->spoofDriver ($ connection , 'mysql ' );
407+ // Bypass the setter validation by spoofing the charset directly.
408+ $ this ->spoofCredential ($ connection , 'charset ' , "utf8' -- " );
409+
410+ // No exception — the regex guard catches it before exec.
411+ $ this ->invokePrivate ($ connection , 'applyCharsetAndCollation ' );
412+ self ::assertTrue (true );
413+ }
414+
415+ private function spoofDriver (Connection $ connection , string $ driver ): void
416+ {
417+ $ this ->spoofCredential ($ connection , 'driver ' , $ driver );
418+ }
419+
420+ private function spoofCredential (Connection $ connection , string $ key , $ value ): void
421+ {
422+ $ ref = new \ReflectionObject ($ connection );
423+ $ prop = $ ref ->getProperty ('credentials ' );
424+ $ prop ->setAccessible (true );
425+ $ creds = $ prop ->getValue ($ connection );
426+ $ creds [$ key ] = $ value ;
427+ $ prop ->setValue ($ connection , $ creds );
428+ }
429+
430+ /**
431+ * @param array<int, mixed> $args
432+ * @return mixed
433+ */
434+ private function invokePrivate (Connection $ connection , string $ method , array $ args = [])
435+ {
436+ $ ref = new \ReflectionObject ($ connection );
437+ $ m = $ ref ->getMethod ($ method );
438+ $ m ->setAccessible (true );
439+
440+ return $ m ->invokeArgs ($ connection , $ args );
441+ }
295442}
0 commit comments