@@ -60,27 +60,37 @@ public function testValidateAcceptsValidConfig(array $config): void
6060
6161 public function testValidateRejectsUnknownKey (): void
6262 {
63- $ this ->expectException (InvalidConfigException::class);
64- $ this ->expectExceptionMessage ('Connection [master]: unsupported config key "query_timeout_ms". ' );
65-
66- ConnectionConfigResolver::validate ('master ' , [
67- 'driver ' => 'mysql ' ,
68- 'host ' => 'localhost ' ,
69- 'database ' => 'app ' ,
70- 'query_timeout_ms ' => 5000 ,
71- ]);
63+ try {
64+ ConnectionConfigResolver::validate ('master ' , [
65+ 'driver ' => 'mysql ' ,
66+ 'host ' => 'localhost ' ,
67+ 'database ' => 'app ' ,
68+ 'query_timeout_ms ' => 5000 ,
69+ ]);
70+ $ this ->fail ('Expected InvalidConfigException ' );
71+ } catch (InvalidConfigException $ e ) {
72+ $ this ->assertSame (
73+ 'Connection [master]: unsupported config key "query_timeout_ms". ' ,
74+ $ e ->getMessage (),
75+ );
76+ }
7277 }
7378
7479 public function testValidateRejectsMistypedKey (): void
7580 {
76- $ this ->expectException (InvalidConfigException::class);
77- $ this ->expectExceptionMessage ('Connection [master]: unsupported config key "databse". ' );
78-
79- ConnectionConfigResolver::validate ('master ' , [
80- 'driver ' => 'mysql ' ,
81- 'host ' => 'localhost ' ,
82- 'databse ' => 'app ' ,
83- ]);
81+ try {
82+ ConnectionConfigResolver::validate ('master ' , [
83+ 'driver ' => 'mysql ' ,
84+ 'host ' => 'localhost ' ,
85+ 'databse ' => 'app ' ,
86+ ]);
87+ $ this ->fail ('Expected InvalidConfigException ' );
88+ } catch (InvalidConfigException $ e ) {
89+ $ this ->assertSame (
90+ 'Connection [master]: unsupported config key "databse". ' ,
91+ $ e ->getMessage (),
92+ );
93+ }
8494 }
8595
8696 /**
@@ -105,60 +115,85 @@ public function testValidateRejectsMissingRequiredKey(string $missingKey): void
105115 ];
106116 unset($ config [$ missingKey ]);
107117
108- $ this ->expectException (InvalidConfigException::class);
109- $ this ->expectExceptionMessage ('Connection [master]: missing required config key " ' . $ missingKey . '". ' );
110-
111- ConnectionConfigResolver::validate ('master ' , $ config );
118+ try {
119+ ConnectionConfigResolver::validate ('master ' , $ config );
120+ $ this ->fail ('Expected InvalidConfigException ' );
121+ } catch (InvalidConfigException $ e ) {
122+ $ this ->assertSame (
123+ 'Connection [master]: missing required config key " ' . $ missingKey . '". ' ,
124+ $ e ->getMessage (),
125+ );
126+ }
112127 }
113128
114129 public function testValidateRejectsUnsupportedDriver (): void
115130 {
116- $ this ->expectException (InvalidConfigException::class);
117- $ this ->expectExceptionMessage ("Connection [master]: unsupported driver \"pgsql \". Only 'mysql' is supported. " );
118-
119- ConnectionConfigResolver::validate ('master ' , [
120- 'driver ' => 'pgsql ' ,
121- 'host ' => 'localhost ' ,
122- 'database ' => 'app ' ,
123- ]);
131+ try {
132+ ConnectionConfigResolver::validate ('master ' , [
133+ 'driver ' => 'pgsql ' ,
134+ 'host ' => 'localhost ' ,
135+ 'database ' => 'app ' ,
136+ ]);
137+ $ this ->fail ('Expected InvalidConfigException ' );
138+ } catch (InvalidConfigException $ e ) {
139+ $ this ->assertSame (
140+ "Connection [master]: unsupported driver \"pgsql \". Only 'mysql' is supported. " ,
141+ $ e ->getMessage (),
142+ );
143+ }
124144 }
125145
126146 public function testValidateRejectsNonStringDriver (): void
127147 {
128- $ this ->expectException (InvalidConfigException::class);
129- $ this ->expectExceptionMessage ('Connection [master]: config key "driver" must be a string. ' );
130-
131- ConnectionConfigResolver::validate ('master ' , [
132- 'driver ' => 1 ,
133- 'host ' => 'localhost ' ,
134- 'database ' => 'app ' ,
135- ]);
148+ try {
149+ ConnectionConfigResolver::validate ('master ' , [
150+ 'driver ' => 1 ,
151+ 'host ' => 'localhost ' ,
152+ 'database ' => 'app ' ,
153+ ]);
154+ $ this ->fail ('Expected InvalidConfigException ' );
155+ } catch (InvalidConfigException $ e ) {
156+ $ this ->assertSame (
157+ 'Connection [master]: config key "driver" must be a string. ' ,
158+ $ e ->getMessage (),
159+ );
160+ }
136161 }
137162
138163 public function testValidateRejectsNonIntPort (): void
139164 {
140- $ this ->expectException (InvalidConfigException::class);
141- $ this ->expectExceptionMessage ('Connection [master]: config key "port" must be an integer. ' );
142-
143- ConnectionConfigResolver::validate ('master ' , [
144- 'driver ' => 'mysql ' ,
145- 'host ' => 'localhost ' ,
146- 'database ' => 'app ' ,
147- 'port ' => '3306 ' ,
148- ]);
165+ try {
166+ ConnectionConfigResolver::validate ('master ' , [
167+ 'driver ' => 'mysql ' ,
168+ 'host ' => 'localhost ' ,
169+ 'database ' => 'app ' ,
170+ 'port ' => '3306 ' ,
171+ ]);
172+ $ this ->fail ('Expected InvalidConfigException ' );
173+ } catch (InvalidConfigException $ e ) {
174+ $ this ->assertSame (
175+ 'Connection [master]: config key "port" must be an integer. ' ,
176+ $ e ->getMessage (),
177+ );
178+ }
149179 }
150180
151181 public function testValidateRejectsNonStringNullableUsername (): void
152182 {
153- $ this ->expectException (InvalidConfigException::class);
154- $ this ->expectExceptionMessage ('Connection [master]: config key "username" must be a string or null. ' );
155-
156- ConnectionConfigResolver::validate ('master ' , [
157- 'driver ' => 'mysql ' ,
158- 'host ' => 'localhost ' ,
159- 'database ' => 'app ' ,
160- 'username ' => 123 ,
161- ]);
183+ try {
184+ ConnectionConfigResolver::validate ('master ' , [
185+ 'driver ' => 'mysql ' ,
186+ 'host ' => 'localhost ' ,
187+ 'database ' => 'app ' ,
188+ 'username ' => 123 ,
189+ ]);
190+ $ this ->fail ('Expected InvalidConfigException ' );
191+ } catch (InvalidConfigException $ e ) {
192+ $ this ->assertSame (
193+ 'Connection [master]: config key "username" must be a string or null. ' ,
194+ $ e ->getMessage (),
195+ );
196+ }
162197 }
163198
164199 /**
@@ -167,77 +202,113 @@ public function testValidateRejectsNonStringNullableUsername(): void
167202 public static function invalidIdentifierProvider (): array
168203 {
169204 return [
170- 'charset with space ' => ['charset ' , 'utf8 mb4 ' , 'must contain only alphanumeric and underscore characters ' ],
171- 'charset with semicolon ' => ['charset ' , 'utf8mb4;DROP ' , 'must contain only alphanumeric and underscore characters ' ],
172- 'collation with hyphen ' => ['collation ' , 'utf8mb4-unicode ' , 'must contain only alphanumeric and underscore characters ' ],
173- 'charset non-string ' => ['charset ' , 123 , 'must be a string ' ],
205+ 'charset with space ' => [
206+ 'charset ' , 'utf8 mb4 ' ,
207+ 'Connection [master]: config key "charset" must contain only alphanumeric and underscore characters, got "utf8 mb4". ' ,
208+ ],
209+ 'charset with semicolon ' => [
210+ 'charset ' , 'utf8mb4;DROP ' ,
211+ 'Connection [master]: config key "charset" must contain only alphanumeric and underscore characters, got "utf8mb4;DROP". ' ,
212+ ],
213+ 'collation with hyphen ' => [
214+ 'collation ' , 'utf8mb4-unicode ' ,
215+ 'Connection [master]: config key "collation" must contain only alphanumeric and underscore characters, got "utf8mb4-unicode". ' ,
216+ ],
217+ 'charset non-string ' => [
218+ 'charset ' , 123 ,
219+ 'Connection [master]: config key "charset" must be a string. ' ,
220+ ],
174221 ];
175222 }
176223
177224 #[DataProvider('invalidIdentifierProvider ' )]
178225 public function testValidateRejectsInvalidIdentifier (string $ key , mixed $ value , string $ expectedMessage ): void
179226 {
180- $ this ->expectException (InvalidConfigException::class);
181- $ this ->expectExceptionMessageMatches ('/ ' . preg_quote ($ expectedMessage , '/ ' ) . '/ ' );
182-
183- ConnectionConfigResolver::validate ('master ' , [
184- 'driver ' => 'mysql ' ,
185- 'host ' => 'localhost ' ,
186- 'database ' => 'app ' ,
187- $ key => $ value ,
188- ]);
227+ try {
228+ ConnectionConfigResolver::validate ('master ' , [
229+ 'driver ' => 'mysql ' ,
230+ 'host ' => 'localhost ' ,
231+ 'database ' => 'app ' ,
232+ $ key => $ value ,
233+ ]);
234+ $ this ->fail ('Expected InvalidConfigException ' );
235+ } catch (InvalidConfigException $ e ) {
236+ $ this ->assertSame ($ expectedMessage , $ e ->getMessage ());
237+ }
189238 }
190239
191240 public function testValidateRejectsNonArrayOptions (): void
192241 {
193- $ this ->expectException (InvalidConfigException::class);
194- $ this ->expectExceptionMessage ('Connection [master]: config key "options" must be an array. ' );
195-
196- ConnectionConfigResolver::validate ('master ' , [
197- 'driver ' => 'mysql ' ,
198- 'host ' => 'localhost ' ,
199- 'database ' => 'app ' ,
200- 'options ' => 'invalid ' ,
201- ]);
242+ try {
243+ ConnectionConfigResolver::validate ('master ' , [
244+ 'driver ' => 'mysql ' ,
245+ 'host ' => 'localhost ' ,
246+ 'database ' => 'app ' ,
247+ 'options ' => 'invalid ' ,
248+ ]);
249+ $ this ->fail ('Expected InvalidConfigException ' );
250+ } catch (InvalidConfigException $ e ) {
251+ $ this ->assertSame (
252+ 'Connection [master]: config key "options" must be an array. ' ,
253+ $ e ->getMessage (),
254+ );
255+ }
202256 }
203257
204258 public function testValidateRejectsStringKeyedOptions (): void
205259 {
206- $ this ->expectException (InvalidConfigException::class);
207- $ this ->expectExceptionMessage ('Connection [master]: config key "options" must be an array with integer (PDO::ATTR_*) keys. ' );
208-
209- ConnectionConfigResolver::validate ('master ' , [
210- 'driver ' => 'mysql ' ,
211- 'host ' => 'localhost ' ,
212- 'database ' => 'app ' ,
213- 'options ' => ['attr_persistent ' => false ],
214- ]);
260+ try {
261+ ConnectionConfigResolver::validate ('master ' , [
262+ 'driver ' => 'mysql ' ,
263+ 'host ' => 'localhost ' ,
264+ 'database ' => 'app ' ,
265+ 'options ' => ['attr_persistent ' => false ],
266+ ]);
267+ $ this ->fail ('Expected InvalidConfigException ' );
268+ } catch (InvalidConfigException $ e ) {
269+ $ this ->assertSame (
270+ 'Connection [master]: config key "options" must be an array with integer (PDO::ATTR_*) keys. ' ,
271+ $ e ->getMessage (),
272+ );
273+ }
215274 }
216275
217276 public function testValidateRejectsIntegerKey (): void
218277 {
219278 // Application::filterStringKeys removes int keys before the config
220279 // reaches the resolver, so this scenario only fires when the resolver
221280 // is called directly. Cover the defensive `is_string($key)` branch.
222- $ this ->expectException (InvalidConfigException::class);
223- $ this ->expectExceptionMessage ('Connection [master]: unsupported config key "0". ' );
224-
225- ConnectionConfigResolver::validate ('master ' , [
226- 'driver ' => 'mysql ' ,
227- 'host ' => 'localhost ' ,
228- 'database ' => 'app ' ,
229- 0 => 'unexpected_int_key ' ,
230- ]);
281+ try {
282+ ConnectionConfigResolver::validate ('master ' , [
283+ 'driver ' => 'mysql ' ,
284+ 'host ' => 'localhost ' ,
285+ 'database ' => 'app ' ,
286+ 0 => 'unexpected_int_key ' ,
287+ ]);
288+ $ this ->fail ('Expected InvalidConfigException ' );
289+ } catch (InvalidConfigException $ e ) {
290+ $ this ->assertSame (
291+ 'Connection [master]: unsupported config key "0". ' ,
292+ $ e ->getMessage (),
293+ );
294+ }
231295 }
232296
233297 public function testValidateIncludesConnectionNameInMessage (): void
234298 {
235- $ this ->expectException (InvalidConfigException::class);
236- $ this ->expectExceptionMessage ('Connection [analytics]: ' );
237-
238- ConnectionConfigResolver::validate ('analytics ' , [
239- 'driver ' => 'mysql ' ,
240- ]);
299+ // Confirms the connection name is propagated into all error messages.
300+ // Asserts the full message so concat-mutation cannot escape.
301+ try {
302+ ConnectionConfigResolver::validate ('analytics ' , [
303+ 'driver ' => 'mysql ' ,
304+ ]);
305+ $ this ->fail ('Expected InvalidConfigException ' );
306+ } catch (InvalidConfigException $ e ) {
307+ $ this ->assertSame (
308+ 'Connection [analytics]: missing required config key "host". ' ,
309+ $ e ->getMessage (),
310+ );
311+ }
241312 }
242313
243314 public function testValidateReturnsValidatedConfigWithValues (): void
@@ -387,18 +458,22 @@ public function testResolvePdoOptionsAppliesCollationWithDefaultCharset(): void
387458
388459 public function testResolvePdoOptionsMergesUserOptions (): void
389460 {
461+ // Multiple user options ensures the resolver returns all entries
462+ // (kills ArrayOneItem mutation that would slice to a single element).
390463 $ validated = ConnectionConfigResolver::validate ('master ' , [
391464 'driver ' => 'mysql ' ,
392465 'host ' => 'localhost ' ,
393466 'database ' => 'app ' ,
394467 'options ' => [
395468 PDO ::ATTR_PERSISTENT => true ,
469+ PDO ::ATTR_AUTOCOMMIT => false ,
396470 ],
397471 ]);
398472
399473 $ options = ConnectionConfigResolver::resolvePdoOptions ($ validated );
400474
401475 $ this ->assertSame (true , $ options [PDO ::ATTR_PERSISTENT ]);
476+ $ this ->assertSame (false , $ options [PDO ::ATTR_AUTOCOMMIT ]);
402477 $ this ->assertSame (2 , $ options [PDO ::ATTR_TIMEOUT ]);
403478 $ this ->assertSame ('SET NAMES utf8mb4 ' , $ options [PdoMysql::ATTR_INIT_COMMAND ]);
404479 }
0 commit comments