1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace TraderInteractive; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use InvalidArgumentException; |
7
|
|
|
use PHPUnit\Framework\TestCase; |
8
|
|
|
use RuntimeException; |
9
|
|
|
use stdClass; |
10
|
|
|
use Throwable; |
11
|
|
|
use TraderInteractive\Exceptions\FilterException; |
12
|
|
|
use TypeError; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* @coversDefaultClass \TraderInteractive\Filterer |
16
|
|
|
* @covers ::__construct |
17
|
|
|
* @covers ::<private> |
18
|
|
|
*/ |
19
|
|
|
final class FiltererTest extends TestCase |
20
|
|
|
{ |
21
|
|
|
public function setUp() |
22
|
|
|
{ |
23
|
|
|
Filterer::setFilterAliases(Filterer::DEFAULT_FILTER_ALIASES); |
24
|
|
|
} |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @test |
28
|
|
|
* @covers ::filter |
29
|
|
|
* @covers ::execute |
30
|
|
|
* @dataProvider provideValidFilterData |
31
|
|
|
* |
32
|
|
|
* @param array $spec The filter specification to be use. |
33
|
|
|
* @param array $input The input to be filtered. |
34
|
|
|
* @param array $options The filterer options |
35
|
|
|
* @param array $expectedResult The expected filterer result. |
36
|
|
|
*/ |
37
|
|
|
public function filter(array $spec, array $input, array $options, array $expectedResult) |
38
|
|
|
{ |
39
|
|
|
$this->assertSame($expectedResult, Filterer::filter($spec, $input, $options)); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
public function provideValidFilterData() : array |
43
|
|
|
{ |
44
|
|
|
return [ |
45
|
|
|
'not required field not present' => [ |
46
|
|
|
'spec' => ['fieldOne' => ['required' => false]], |
47
|
|
|
'input' => [], |
48
|
|
|
'options' => [], |
49
|
|
|
'result' => [true, [], null, []], |
50
|
|
|
], |
51
|
|
|
'Required With A Default Without Input' => [ |
52
|
|
|
'spec' => ['fieldOne' => ['required' => true, 'default' => 'theDefault']], |
53
|
|
|
'input' => [], |
54
|
|
|
'options' => [], |
55
|
|
|
'result' => [true, ['fieldOne' => 'theDefault'], null, []], |
56
|
|
|
], |
57
|
|
|
'Required With A Null Default Without Input' => [ |
58
|
|
|
'spec' => ['fieldOne' => ['required' => true, 'default' => null]], |
59
|
|
|
'input' => [], |
60
|
|
|
'options' => [], |
61
|
|
|
'result' => [true, ['fieldOne' => null], null, []], |
62
|
|
|
], |
63
|
|
|
'Required With A Default With Input' => [ |
64
|
|
|
'spec' => ['fieldOne' => ['required' => true, 'default' => 'theDefault']], |
65
|
|
|
'input' => ['fieldOne' => 'notTheDefault'], |
66
|
|
|
'options' => [], |
67
|
|
|
'result' => [true, ['fieldOne' => 'notTheDefault'], null, []], |
68
|
|
|
], |
69
|
|
|
'Not Required With A Default Without Input' => [ |
70
|
|
|
'spec' => ['fieldOne' => ['default' => 'theDefault']], |
71
|
|
|
'input' => [], |
72
|
|
|
'options' => [], |
73
|
|
|
'result' => [true, ['fieldOne' => 'theDefault'], null, []], |
74
|
|
|
], |
75
|
|
|
'Not Required With A Default With Input' => [ |
76
|
|
|
'spec' => ['fieldOne' => ['default' => 'theDefault']], |
77
|
|
|
'input' => ['fieldOne' => 'notTheDefault'], |
78
|
|
|
'options' => [], |
79
|
|
|
'result' => [true, ['fieldOne' => 'notTheDefault'], null, []], |
80
|
|
|
], |
81
|
|
|
'Required Fail' => [ |
82
|
|
|
'spec' => ['fieldOne' => ['required' => true]], |
83
|
|
|
'input' => [], |
84
|
|
|
'options' => [], |
85
|
|
|
'result' => [false, null, "Field 'fieldOne' was required and not present", []], |
86
|
|
|
], |
87
|
|
|
'Required Default Pass' => [ |
88
|
|
|
'spec' => ['fieldOne' => []], |
89
|
|
|
'input' => [], |
90
|
|
|
'options' => [], |
91
|
|
|
'result' => [true, [], null, []], |
92
|
|
|
], |
93
|
|
|
'requiredDefaultFail' => [ |
94
|
|
|
'spec' => ['fieldOne' => []], |
95
|
|
|
'input' => [], |
96
|
|
|
'options' => ['defaultRequired' => true], |
97
|
|
|
'result' => [false, null, "Field 'fieldOne' was required and not present", []], |
98
|
|
|
], |
99
|
|
|
'filterPass' => [ |
100
|
|
|
'spec' => ['fieldOne' => [['floatval']]], |
101
|
|
|
'input' => ['fieldOne' => '3.14'], |
102
|
|
|
'options' => [], |
103
|
|
|
'result' => [true, ['fieldOne' => 3.14], null, []], |
104
|
|
|
], |
105
|
|
|
'filterDefaultShortNamePass' => [ |
106
|
|
|
'spec' => ['fieldOne' => [['float']]], |
107
|
|
|
'input' => ['fieldOne' => '3.14'], |
108
|
|
|
'options' => [], |
109
|
|
|
'result' => [true, ['fieldOne' => 3.14], null, []], |
110
|
|
|
], |
111
|
|
|
'filterFail' => [ |
112
|
|
|
'spec' => ['fieldOne' => [['\TraderInteractive\FiltererTest::failingFilter']]], |
113
|
|
|
'input' => ['fieldOne' => 'valueOne'], |
114
|
|
|
'options' => [], |
115
|
|
|
'result' => [ |
116
|
|
|
false, |
117
|
|
|
null, |
118
|
|
|
"Field 'fieldOne' with value 'valueOne' failed filtering, message 'i failed'", |
119
|
|
|
[], |
120
|
|
|
], |
121
|
|
|
], |
122
|
|
|
'chainPass' => [ |
123
|
|
|
'spec' => ['fieldOne' => [['trim', 'a'], ['floatval']]], |
124
|
|
|
'input' => ['fieldOne' => 'a3.14'], |
125
|
|
|
'options' => [], |
126
|
|
|
'result' => [true, ['fieldOne' => 3.14], null, []], |
127
|
|
|
], |
128
|
|
|
'chainFail' => [ |
129
|
|
|
'spec' => ['fieldOne' => [['trim'], ['\TraderInteractive\FiltererTest::failingFilter']]], |
130
|
|
|
'input' => ['fieldOne' => 'the value'], |
131
|
|
|
'options' => [], |
132
|
|
|
'result' => [ |
133
|
|
|
false, |
134
|
|
|
null, |
135
|
|
|
"Field 'fieldOne' with value 'the value' failed filtering, message 'i failed'", |
136
|
|
|
[], |
137
|
|
|
], |
138
|
|
|
], |
139
|
|
|
'multiInputPass' => [ |
140
|
|
|
'spec' => ['fieldOne' => [['trim']], 'fieldTwo' => [['strtoupper']]], |
141
|
|
|
'input' => ['fieldOne' => ' value', 'fieldTwo' => 'bob'], |
142
|
|
|
'options' => [], |
143
|
|
|
'result' => [true, ['fieldOne' => 'value', 'fieldTwo' => 'BOB'], null, []], |
144
|
|
|
], |
145
|
|
|
'multiInputFail' => [ |
146
|
|
|
'spec' => [ |
147
|
|
|
'fieldOne' => [['\TraderInteractive\FiltererTest::failingFilter']], |
148
|
|
|
'fieldTwo' => [['\TraderInteractive\FiltererTest::failingFilter']], |
149
|
|
|
], |
150
|
|
|
'input' => ['fieldOne' => 'value one', 'fieldTwo' => 'value two'], |
151
|
|
|
'options' => [], |
152
|
|
|
'result' => [ |
153
|
|
|
false, |
154
|
|
|
null, |
155
|
|
|
"Field 'fieldOne' with value 'value one' failed filtering, message 'i failed'\n" |
156
|
|
|
. "Field 'fieldTwo' with value 'value two' failed filtering, message 'i failed'", |
157
|
|
|
[], |
158
|
|
|
], |
159
|
|
|
], |
160
|
|
|
'emptyFilter' => [ |
161
|
|
|
'spec' => ['fieldOne' => [[]]], |
162
|
|
|
'input' => ['fieldOne' => 0], |
163
|
|
|
'options' => [], |
164
|
|
|
'result' => [true, ['fieldOne' => 0], null, []], |
165
|
|
|
], |
166
|
|
|
'unknownsAllowed' => [ |
167
|
|
|
'spec' => [], |
168
|
|
|
'input'=> ['fieldTwo' => 0], |
169
|
|
|
'options' => ['allowUnknowns' => true], |
170
|
|
|
'result' => [true, [], null, ['fieldTwo' => 0]], |
171
|
|
|
], |
172
|
|
|
'unknownsNotAllowed' => [ |
173
|
|
|
'spec' => [], |
174
|
|
|
'input' => ['fieldTwo' => 0], |
175
|
|
|
'options' => [], |
176
|
|
|
'result' => [false, null, "Field 'fieldTwo' with value '0' is unknown", ['fieldTwo' => 0]], |
177
|
|
|
], |
178
|
|
|
'objectFilter' => [ |
179
|
|
|
'spec' => ['fieldOne' => [[[$this, 'passingFilter']]]], |
180
|
|
|
'input' => ['fieldOne' => 'foo'], |
181
|
|
|
'options' => [], |
182
|
|
|
'result' => [true, ['fieldOne' => 'fooboo'], null, []], |
183
|
|
|
], |
184
|
|
|
'filterWithCustomError' => [ |
185
|
|
|
'spec' => [ |
186
|
|
|
'fieldOne' => [ |
187
|
|
|
'error' => 'My custom error message', |
188
|
|
|
['\TraderInteractive\FiltererTest::failingFilter'], |
189
|
|
|
], |
190
|
|
|
], |
191
|
|
|
'input' => ['fieldOne' => 'valueOne'], |
192
|
|
|
'options' => [], |
193
|
|
|
'result' => [false, null, 'My custom error message', []], |
194
|
|
|
], |
195
|
|
|
'filterWithCustomErrorContainingValuePlaceholder' => [ |
196
|
|
|
'spec' => [ |
197
|
|
|
'fieldOne' => [ |
198
|
|
|
'error' => "The value '{value}' is invalid.", |
199
|
|
|
['uint'], |
200
|
|
|
], |
201
|
|
|
], |
202
|
|
|
'input' => ['fieldOne' => 'abc'], |
203
|
|
|
'options' => [], |
204
|
|
|
'result' => [false, null, "The value 'abc' is invalid.", []], |
205
|
|
|
], |
206
|
|
|
'arrayizeAliasIsCalledProperly' => [ |
207
|
|
|
'spec' => ['field' => [['arrayize']]], |
208
|
|
|
'input' => ['field' => 'a string value'], |
209
|
|
|
'options' => [], |
210
|
|
|
'result' => [true, ['field' => ['a string value']], null, []], |
211
|
|
|
], |
212
|
|
|
'concatAliasIsCalledProperly' => [ |
213
|
|
|
'spec' => ['field' => [['concat', '%', '%']]], |
214
|
|
|
'input' => ['field' => 'value'], |
215
|
|
|
'options' => [], |
216
|
|
|
'result' => [true, ['field' => '%value%'], null, []], |
217
|
|
|
], |
218
|
|
|
'translate alias' => [ |
219
|
|
|
'spec' => ['field' => [['translate', ['active' => 'A', 'inactive' => 'I']]]], |
220
|
|
|
'input' => ['field' => 'inactive'], |
221
|
|
|
'options' => [], |
222
|
|
|
'result' => [true, ['field' => 'I'], null, []], |
223
|
|
|
], |
224
|
|
|
'redact alias' => [ |
225
|
|
|
'spec' => ['field' => [['redact', ['other'], '*']]], |
226
|
|
|
'input' => ['field' => 'one or other'], |
227
|
|
|
'options' => [], |
228
|
|
|
'result' => [true, ['field' => 'one or *****'], null, []], |
229
|
|
|
], |
230
|
|
|
'compress-string alias' => [ |
231
|
|
|
'spec' => ['field' => [['compress-string']]], |
232
|
|
|
'input' => ['field' => ' a string with extra spaces '], |
233
|
|
|
'options' => [], |
234
|
|
|
'result' => [true, ['field' => 'a string with extra spaces'], null, []], |
235
|
|
|
], |
236
|
|
|
'compress-string alias include newlines' => [ |
237
|
|
|
'spec' => ['field' => [['compress-string', true]]], |
238
|
|
|
'input' => ['field' => " a string\n with\nnewlines and extra spaces\n "], |
239
|
|
|
'options' => [], |
240
|
|
|
'result' => [true, ['field' => 'a string with newlines and extra spaces'], null, []], |
241
|
|
|
], |
242
|
|
|
'conflicts with single' => [ |
243
|
|
|
'spec' => [ |
244
|
|
|
'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']], |
245
|
|
|
'fieldTwo' => [['string']], |
246
|
|
|
'fieldThree' => [FilterOptions::CONFLICTS_WITH => 'fieldOne', ['string']], |
247
|
|
|
], |
248
|
|
|
'input' => [ |
249
|
|
|
'fieldOne' => 'abc', |
250
|
|
|
'fieldTwo' => '123', |
251
|
|
|
'fieldThree' => 'xyz', |
252
|
|
|
], |
253
|
|
|
'options' => [], |
254
|
|
|
'result' => [ |
255
|
|
|
false, |
256
|
|
|
null, |
257
|
|
|
"Field 'fieldOne' cannot be given if field 'fieldThree' is present.\n" |
258
|
|
|
. "Field 'fieldThree' cannot be given if field 'fieldOne' is present.", |
259
|
|
|
[], |
260
|
|
|
], |
261
|
|
|
], |
262
|
|
|
'conflicts with multiple' => [ |
263
|
|
|
'spec' => [ |
264
|
|
|
'fieldOne' => [FilterOptions::CONFLICTS_WITH => ['fieldTwo', 'fieldThree'], ['string']], |
265
|
|
|
'fieldTwo' => [['string']], |
266
|
|
|
'fieldThree' => [['string']], |
267
|
|
|
], |
268
|
|
|
'input' => [ |
269
|
|
|
'fieldOne' => 'abc', |
270
|
|
|
'fieldTwo' => '123', |
271
|
|
|
], |
272
|
|
|
'options' => [], |
273
|
|
|
'result' => [ |
274
|
|
|
false, |
275
|
|
|
null, |
276
|
|
|
"Field 'fieldOne' cannot be given if field 'fieldTwo' is present.", |
277
|
|
|
[], |
278
|
|
|
], |
279
|
|
|
], |
280
|
|
|
'conflicts with not present' => [ |
281
|
|
|
'spec' => [ |
282
|
|
|
'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']], |
283
|
|
|
'fieldTwo' => [['string']], |
284
|
|
|
], |
285
|
|
|
'input' => [ |
286
|
|
|
'fieldOne' => 'abc', |
287
|
|
|
'fieldTwo' => '123', |
288
|
|
|
], |
289
|
|
|
'options' => [], |
290
|
|
|
'result' => [ |
291
|
|
|
true, |
292
|
|
|
[ |
293
|
|
|
'fieldOne' => 'abc', |
294
|
|
|
'fieldTwo' => '123', |
295
|
|
|
], |
296
|
|
|
null, |
297
|
|
|
[], |
298
|
|
|
], |
299
|
|
|
], |
300
|
|
|
'uses' => [ |
301
|
|
|
'spec' => [ |
302
|
|
|
'fieldOne' => [['uint']], |
303
|
|
|
'fieldTwo' => [ |
304
|
|
|
['uint'], |
305
|
|
|
[ |
306
|
|
|
FilterOptions::USES => ['fieldOne'], |
307
|
|
|
function (int $input, int $fieldOneValue) : int { |
308
|
|
|
return $input * $fieldOneValue; |
309
|
|
|
}, |
310
|
|
|
], |
311
|
|
|
], |
312
|
|
|
], |
313
|
|
|
'input' => [ |
314
|
|
|
'fieldOne' => '5', |
315
|
|
|
'fieldTwo' => '2', |
316
|
|
|
], |
317
|
|
|
'options' => [], |
318
|
|
|
'result' => [ |
319
|
|
|
true, |
320
|
|
|
[ |
321
|
|
|
'fieldOne' => 5, |
322
|
|
|
'fieldTwo' => 10, |
323
|
|
|
], |
324
|
|
|
null, |
325
|
|
|
[], |
326
|
|
|
], |
327
|
|
|
], |
328
|
|
|
'input order does not matter for uses' => [ |
329
|
|
|
'spec' => [ |
330
|
|
|
'fieldOne' => [['uint']], |
331
|
|
|
'fieldTwo' => [ |
332
|
|
|
['uint'], |
333
|
|
|
[ |
334
|
|
|
FilterOptions::USES => ['fieldOne'], |
335
|
|
|
function (int $input, int $fieldOneValue) : int { |
336
|
|
|
return $input * $fieldOneValue; |
337
|
|
|
}, |
338
|
|
|
], |
339
|
|
|
], |
340
|
|
|
], |
341
|
|
|
'input' => [ |
342
|
|
|
'fieldTwo' => '2', |
343
|
|
|
'fieldOne' => '5', |
344
|
|
|
], |
345
|
|
|
'options' => [], |
346
|
|
|
'result' => [ |
347
|
|
|
true, |
348
|
|
|
[ |
349
|
|
|
'fieldOne' => 5, |
350
|
|
|
'fieldTwo' => 10, |
351
|
|
|
], |
352
|
|
|
null, |
353
|
|
|
[], |
354
|
|
|
], |
355
|
|
|
], |
356
|
|
|
'uses missing field' => [ |
357
|
|
|
'spec' => [ |
358
|
|
|
'fieldOne' => [['uint']], |
359
|
|
|
'fieldTwo' => [ |
360
|
|
|
['uint'], |
361
|
|
|
[ |
362
|
|
|
FilterOptions::USES => ['fieldOne'], |
363
|
|
|
function (int $input, int $fieldOneValue) : int { |
364
|
|
|
return $input * $fieldOneValue; |
365
|
|
|
}, |
366
|
|
|
], |
367
|
|
|
], |
368
|
|
|
], |
369
|
|
|
'input' => [ |
370
|
|
|
'fieldTwo' => '2', |
371
|
|
|
], |
372
|
|
|
'options' => [], |
373
|
|
|
'result' => [ |
374
|
|
|
false, |
375
|
|
|
null, |
376
|
|
|
"Field 'fieldTwo' with value '2' failed filtering, message 'fieldTwo uses fieldOne but fieldOne was" |
377
|
|
|
. " not given.'", |
378
|
|
|
[], |
379
|
|
|
], |
380
|
|
|
], |
381
|
|
|
'returnOnNull filter option' => [ |
382
|
|
|
'spec' => ['field' => [FilterOptions::RETURN_ON_NULL => true, ['string', true], ['string']]], |
383
|
|
|
'input' => ['field' => null], |
384
|
|
|
'options' => [], |
385
|
|
|
'result' => [true, ['field' => null], null, []], |
386
|
|
|
], |
387
|
|
|
]; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* @test |
392
|
|
|
* @covers ::execute |
393
|
|
|
*/ |
394
|
|
|
public function executeThrowsOnError() |
395
|
|
|
{ |
396
|
|
|
$exception = new RuntimeException('the error'); |
397
|
|
|
$this->expectException(RuntimeException::class); |
398
|
|
|
$this->expectExceptionMessage($exception->getMessage()); |
399
|
|
|
$filter = function () use ($exception) { |
400
|
|
|
throw $exception; |
401
|
|
|
}; |
402
|
|
|
|
403
|
|
|
$specification = [ |
404
|
|
|
'id' => [ |
405
|
|
|
FilterOptions::THROW_ON_ERROR => true, |
406
|
|
|
[$filter], |
407
|
|
|
], |
408
|
|
|
]; |
409
|
|
|
$filterer = new Filterer($specification); |
410
|
|
|
$filterer->execute(['id' => 1]); |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* @test |
415
|
|
|
* @covers ::execute |
416
|
|
|
*/ |
417
|
|
View Code Duplication |
public function executeValidatesThrowsOnError() |
|
|
|
|
418
|
|
|
{ |
419
|
|
|
$this->expectException(InvalidArgumentException::class); |
420
|
|
|
$this->expectExceptionMessage( |
421
|
|
|
sprintf(Filterer::INVALID_BOOLEAN_FILTER_OPTION, FilterOptions::THROW_ON_ERROR, 'id') |
422
|
|
|
); |
423
|
|
|
$specification = [ |
424
|
|
|
'id' => [ |
425
|
|
|
FilterOptions::THROW_ON_ERROR => 'abc', |
426
|
|
|
['uint'], |
427
|
|
|
], |
428
|
|
|
]; |
429
|
|
|
$filterer = new Filterer($specification); |
430
|
|
|
$filterer->execute(['id' => 1]); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* @test |
435
|
|
|
* @covers ::execute |
436
|
|
|
*/ |
437
|
|
View Code Duplication |
public function executeValidatesReturnOnNull() |
|
|
|
|
438
|
|
|
{ |
439
|
|
|
$this->expectException(InvalidArgumentException::class); |
440
|
|
|
$this->expectExceptionMessage( |
441
|
|
|
sprintf(Filterer::INVALID_BOOLEAN_FILTER_OPTION, FilterOptions::RETURN_ON_NULL, 'id') |
442
|
|
|
); |
443
|
|
|
$specification = [ |
444
|
|
|
'id' => [ |
445
|
|
|
FilterOptions::RETURN_ON_NULL => 'abc', |
446
|
|
|
['uint'], |
447
|
|
|
], |
448
|
|
|
]; |
449
|
|
|
$filterer = new Filterer($specification); |
450
|
|
|
$filterer->execute(['id' => 1]); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
/** |
454
|
|
|
* @test |
455
|
|
|
* @covers ::filter |
456
|
|
|
* @covers ::execute |
457
|
|
|
*/ |
458
|
|
|
public function filterReturnsResponseType() |
459
|
|
|
{ |
460
|
|
|
$specification = ['id' => [['uint']]]; |
461
|
|
|
$input = ['id' => 1]; |
462
|
|
|
$options = ['responseType' => Filterer::RESPONSE_TYPE_FILTER]; |
463
|
|
|
|
464
|
|
|
$result = Filterer::filter($specification, $input, $options); |
465
|
|
|
|
466
|
|
|
$this->assertInstanceOf(FilterResponse::class, $result); |
467
|
|
|
$this->assertSame(true, $result->success); |
468
|
|
|
$this->assertSame($input, $result->filteredValue); |
469
|
|
|
$this->assertSame([], $result->errors); |
470
|
|
|
$this->assertSame(null, $result->errorMessage); |
471
|
|
|
$this->assertSame([], $result->unknowns); |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* @test |
476
|
|
|
* @covers ::filter |
477
|
|
|
* @covers ::execute |
478
|
|
|
*/ |
479
|
|
|
public function filterReturnsResponseTypeWithErrors() |
480
|
|
|
{ |
481
|
|
|
$specification = ['name' => [['string']]]; |
482
|
|
|
$input = ['name' => 'foo', 'id' => 1]; |
483
|
|
|
$options = ['responseType' => Filterer::RESPONSE_TYPE_FILTER]; |
484
|
|
|
|
485
|
|
|
$result = Filterer::filter($specification, $input, $options); |
486
|
|
|
|
487
|
|
|
$this->assertInstanceOf(FilterResponse::class, $result); |
488
|
|
|
$this->assertSame(false, $result->success); |
489
|
|
|
$this->assertSame(['name' => 'foo'], $result->filteredValue); |
490
|
|
|
$this->assertSame(['id' => "Field 'id' with value '1' is unknown"], $result->errors); |
491
|
|
|
$this->assertSame("Field 'id' with value '1' is unknown", $result->errorMessage); |
492
|
|
|
$this->assertSame(['id' => 1], $result->unknowns); |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* @test |
497
|
|
|
* @covers ::filter |
498
|
|
|
* @covers ::execute |
499
|
|
|
* @covers ::setFilterAliases |
500
|
|
|
*/ |
501
|
|
|
public function filterCustomShortNamePass() |
502
|
|
|
{ |
503
|
|
|
Filterer::setFilterAliases(['fval' => 'floatval']); |
504
|
|
|
$result = Filterer::filter(['fieldOne' => [['fval']]], ['fieldOne' => '3.14']); |
505
|
|
|
$this->assertSame([true, ['fieldOne' => 3.14], null, []], $result); |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
/** |
509
|
|
|
* @test |
510
|
|
|
* @covers ::filter |
511
|
|
|
* @covers ::execute |
512
|
|
|
* @covers ::setFilterAliases |
513
|
|
|
* @covers ::getFilterAliases |
514
|
|
|
*/ |
515
|
|
|
public function filterGetSetKnownFilters() |
516
|
|
|
{ |
517
|
|
|
$knownFilters = ['lower' => 'strtolower', 'upper' => 'strtoupper']; |
518
|
|
|
Filterer::setFilterAliases($knownFilters); |
519
|
|
|
$this->assertSame($knownFilters, Filterer::getFilterAliases()); |
520
|
|
|
} |
521
|
|
|
|
522
|
|
|
/** |
523
|
|
|
* @test |
524
|
|
|
* @covers ::setFilterAliases |
525
|
|
|
*/ |
526
|
|
|
public function setBadFilterAliases() |
527
|
|
|
{ |
528
|
|
|
$originalAliases = Filterer::getFilterAliases(); |
529
|
|
|
|
530
|
|
|
$actualThrowable = null; |
531
|
|
|
try { |
532
|
|
|
Filterer::setFilterAliases(['foo' => 'not callable']); |
533
|
|
|
} catch (Throwable $throwable) { |
534
|
|
|
$actualThrowable = $throwable; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
$this->assertSame($originalAliases, Filterer::getFilterAliases()); |
538
|
|
|
$this->assertInstanceOf(TypeError::class, $actualThrowable); |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* @test |
543
|
|
|
* @covers ::filter |
544
|
|
|
* @covers ::execute |
545
|
|
|
* @expectedException Exception |
546
|
|
|
* @expectedExceptionMessage Function 'boo' for field 'foo' is not callable |
547
|
|
|
*/ |
548
|
|
|
public function notCallable() |
549
|
|
|
{ |
550
|
|
|
Filterer::filter(['foo' => [['boo']]], ['foo' => 0]); |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
/** |
554
|
|
|
* @test |
555
|
|
|
* @covers ::filter |
556
|
|
|
* @covers ::execute |
557
|
|
|
* @expectedException InvalidArgumentException |
558
|
|
|
* @expectedExceptionMessage 'allowUnknowns' option was not a bool |
559
|
|
|
*/ |
560
|
|
|
public function allowUnknownsNotBool() |
561
|
|
|
{ |
562
|
|
|
Filterer::filter([], [], ['allowUnknowns' => 1]); |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
/** |
566
|
|
|
* @test |
567
|
|
|
* @covers ::filter |
568
|
|
|
* @covers ::execute |
569
|
|
|
* @expectedException InvalidArgumentException |
570
|
|
|
* @expectedExceptionMessage 'defaultRequired' option was not a bool |
571
|
|
|
*/ |
572
|
|
|
public function defaultRequiredNotBool() |
573
|
|
|
{ |
574
|
|
|
Filterer::filter([], [], ['defaultRequired' => 1]); |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
/** |
578
|
|
|
* @test |
579
|
|
|
* @covers ::filter |
580
|
|
|
* @covers ::execute |
581
|
|
|
*/ |
582
|
|
|
public function filterThrowsExceptionOnInvalidResponseType() |
583
|
|
|
{ |
584
|
|
|
$this->expectException(InvalidArgumentException::class); |
585
|
|
|
$this->expectExceptionMessage("'responseType' was not a recognized value"); |
586
|
|
|
|
587
|
|
|
Filterer::filter([], [], ['responseType' => 'invalid']); |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* @test |
592
|
|
|
* @covers ::filter |
593
|
|
|
* @covers ::execute |
594
|
|
|
* @expectedException InvalidArgumentException |
595
|
|
|
* @expectedExceptionMessage filters for field 'boo' was not a array |
596
|
|
|
*/ |
597
|
|
|
public function filtersNotArrayInLeftOverSpec() |
598
|
|
|
{ |
599
|
|
|
Filterer::filter(['boo' => 1], []); |
600
|
|
|
} |
601
|
|
|
|
602
|
|
|
/** |
603
|
|
|
* @test |
604
|
|
|
* @covers ::filter |
605
|
|
|
* @covers ::execute |
606
|
|
|
* @expectedException InvalidArgumentException |
607
|
|
|
* @expectedExceptionMessage filters for field 'boo' was not a array |
608
|
|
|
*/ |
609
|
|
|
public function filtersNotArrayWithInput() |
610
|
|
|
{ |
611
|
|
|
Filterer::filter(['boo' => 1], ['boo' => 'notUnderTest']); |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
/** |
615
|
|
|
* @test |
616
|
|
|
* @covers ::filter |
617
|
|
|
* @covers ::execute |
618
|
|
|
* @expectedException InvalidArgumentException |
619
|
|
|
* @expectedExceptionMessage filter for field 'boo' was not a array |
620
|
|
|
*/ |
621
|
|
|
public function filterNotArray() |
622
|
|
|
{ |
623
|
|
|
Filterer::filter(['boo' => [1]], ['boo' => 'notUnderTest']); |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* @test |
628
|
|
|
* @covers ::filter |
629
|
|
|
* @covers ::execute |
630
|
|
|
* @expectedException InvalidArgumentException |
631
|
|
|
* @expectedExceptionMessage 'required' for field 'boo' was not a bool |
632
|
|
|
*/ |
633
|
|
|
public function requiredNotBool() |
634
|
|
|
{ |
635
|
|
|
Filterer::filter(['boo' => ['required' => 1]], []); |
636
|
|
|
} |
637
|
|
|
|
638
|
|
|
/** |
639
|
|
|
* @test |
640
|
|
|
* @covers ::registerAlias |
641
|
|
|
* @expectedException InvalidArgumentException |
642
|
|
|
* @expectedExceptionMessage $alias was not a string or int |
643
|
|
|
*/ |
644
|
|
|
public function registerAliasAliasNotString() |
645
|
|
|
{ |
646
|
|
|
Filterer::registerAlias(true, 'strtolower'); |
|
|
|
|
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
/** |
650
|
|
|
* @test |
651
|
|
|
* @covers ::registerAlias |
652
|
|
|
* @expectedException Exception |
653
|
|
|
* @expectedExceptionMessage Alias 'upper' exists |
654
|
|
|
*/ |
655
|
|
|
public function registerExistingAliasOverwriteFalse() |
656
|
|
|
{ |
657
|
|
|
Filterer::setFilterAliases([]); |
658
|
|
|
Filterer::registerAlias('upper', 'strtoupper'); |
659
|
|
|
Filterer::registerAlias('upper', 'strtoupper', false); |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
/** |
663
|
|
|
* @test |
664
|
|
|
* @covers ::registerAlias |
665
|
|
|
*/ |
666
|
|
|
public function registerExistingAliasOverwriteTrue() |
667
|
|
|
{ |
668
|
|
|
Filterer::setFilterAliases(['upper' => 'strtoupper', 'lower' => 'strtolower']); |
669
|
|
|
Filterer::registerAlias('upper', 'ucfirst', true); |
670
|
|
|
$this->assertSame(['upper' => 'ucfirst', 'lower' => 'strtolower'], Filterer::getFilterAliases()); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
public static function failingFilter() |
674
|
|
|
{ |
675
|
|
|
throw new Exception('i failed'); |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
public static function passingFilter($value) |
679
|
|
|
{ |
680
|
|
|
return $value . 'boo'; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
/** |
684
|
|
|
* Verify behavior of filter() when 'error' is not a string value. |
685
|
|
|
* |
686
|
|
|
* @test |
687
|
|
|
* @covers ::filter |
688
|
|
|
* @covers ::execute |
689
|
|
|
* @expectedException InvalidArgumentException |
690
|
|
|
* @expectedExceptionMessage error for field 'fieldOne' was not a non-empty string |
691
|
|
|
* |
692
|
|
|
* @return void |
693
|
|
|
*/ |
694
|
|
View Code Duplication |
public function filterWithNonStringError() |
|
|
|
|
695
|
|
|
{ |
696
|
|
|
Filterer::filter( |
697
|
|
|
['fieldOne' => [['strtoupper'], 'error' => new stdClass()]], |
698
|
|
|
['fieldOne' => 'valueOne'] |
699
|
|
|
); |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* Verify behavior of filter() when 'error' is an empty string. |
704
|
|
|
* |
705
|
|
|
* @test |
706
|
|
|
* @covers ::filter |
707
|
|
|
* @covers ::execute |
708
|
|
|
* @expectedException InvalidArgumentException |
709
|
|
|
* @expectedExceptionMessage error for field 'fieldOne' was not a non-empty string |
710
|
|
|
* |
711
|
|
|
* @return void |
712
|
|
|
*/ |
713
|
|
View Code Duplication |
public function filterWithEmptyStringError() |
|
|
|
|
714
|
|
|
{ |
715
|
|
|
Filterer::filter( |
716
|
|
|
['fieldOne' => [['strtoupper'], 'error' => "\n \t"]], |
717
|
|
|
['fieldOne' => 'valueOne'] |
718
|
|
|
); |
719
|
|
|
} |
720
|
|
|
|
721
|
|
|
/** |
722
|
|
|
* @test |
723
|
|
|
* @covers ::ofScalars |
724
|
|
|
*/ |
725
|
|
|
public function ofScalars() |
726
|
|
|
{ |
727
|
|
|
$this->assertSame([1, 2], Filterer::ofScalars(['1', '2'], [['uint']])); |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
/** |
731
|
|
|
* @test |
732
|
|
|
* @covers ::ofScalars |
733
|
|
|
*/ |
734
|
|
|
public function ofScalarsChained() |
735
|
|
|
{ |
736
|
|
|
$this->assertSame([3.3, 5.5], Filterer::ofScalars(['a3.3', 'a5.5'], [['trim', 'a'], ['floatval']])); |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
/** |
740
|
|
|
* @test |
741
|
|
|
* @covers ::ofScalars |
742
|
|
|
*/ |
743
|
|
|
public function ofScalarsWithMeaninglessKeys() |
744
|
|
|
{ |
745
|
|
|
$this->assertSame(['key1' => 1, 'key2' => 2], Filterer::ofScalars(['key1' => '1', 'key2' => '2'], [['uint']])); |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
/** |
749
|
|
|
* @test |
750
|
|
|
* @covers ::ofScalars |
751
|
|
|
*/ |
752
|
|
View Code Duplication |
public function ofScalarsFail() |
|
|
|
|
753
|
|
|
{ |
754
|
|
|
try { |
755
|
|
|
Filterer::ofScalars(['1', [], new stdClass], [['string']]); |
756
|
|
|
$this->fail(); |
757
|
|
|
} catch (FilterException $e) { |
758
|
|
|
$expected = <<<TXT |
759
|
|
|
Field '1' with value 'array ( |
760
|
|
|
)' failed filtering, message 'Value 'array ( |
761
|
|
|
)' is not a string' |
762
|
|
|
Field '2' with value 'stdClass::__set_state(array( |
763
|
|
|
))' failed filtering, message 'Value 'stdClass::__set_state(array( |
764
|
|
|
))' is not a string' |
765
|
|
|
TXT; |
766
|
|
|
$this->assertSame($expected, $e->getMessage()); |
767
|
|
|
} |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
/** |
771
|
|
|
* @test |
772
|
|
|
* @covers ::ofArrays |
773
|
|
|
*/ |
774
|
|
|
public function ofArrays() |
775
|
|
|
{ |
776
|
|
|
$expected = [['key' => 1], ['key' => 2]]; |
777
|
|
|
$this->assertSame($expected, Filterer::ofArrays([['key' => '1'], ['key' => '2']], ['key' => [['uint']]])); |
778
|
|
|
} |
779
|
|
|
|
780
|
|
|
/** |
781
|
|
|
* @test |
782
|
|
|
* @covers ::ofArrays |
783
|
|
|
*/ |
784
|
|
|
public function ofArraysChained() |
785
|
|
|
{ |
786
|
|
|
$expected = [['key' => 3.3], ['key' => 5.5]]; |
787
|
|
|
$spec = ['key' => [['trim', 'a'], ['floatval']]]; |
788
|
|
|
$this->assertSame($expected, Filterer::ofArrays([['key' => 'a3.3'], ['key' => 'a5.5']], $spec)); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
/** |
792
|
|
|
* @test |
793
|
|
|
* @covers ::ofArrays |
794
|
|
|
*/ |
795
|
|
View Code Duplication |
public function ofArraysRequiredAndUnknown() |
|
|
|
|
796
|
|
|
{ |
797
|
|
|
try { |
798
|
|
|
Filterer::ofArrays([['key' => '1'], ['key2' => '2']], ['key' => ['required' => true, ['uint']]]); |
799
|
|
|
$this->fail(); |
800
|
|
|
} catch (Exception $e) { |
801
|
|
|
$expected = "Field 'key' was required and not present\nField 'key2' with value '2' is unknown"; |
802
|
|
|
$this->assertSame($expected, $e->getMessage()); |
803
|
|
|
} |
804
|
|
|
} |
805
|
|
|
|
806
|
|
|
/** |
807
|
|
|
* @test |
808
|
|
|
* @covers ::ofArrays |
809
|
|
|
*/ |
810
|
|
|
public function ofArraysFail() |
811
|
|
|
{ |
812
|
|
|
try { |
813
|
|
|
Filterer::ofArrays( |
814
|
|
|
[['key' => new stdClass], ['key' => []], ['key' => null], 'key'], |
815
|
|
|
['key' => [['string']]] |
816
|
|
|
); |
817
|
|
|
$this->fail(); |
818
|
|
|
} catch (FilterException $e) { |
819
|
|
|
$expected = <<<TXT |
820
|
|
|
Field 'key' with value 'stdClass::__set_state(array( |
821
|
|
|
))' failed filtering, message 'Value 'stdClass::__set_state(array( |
822
|
|
|
))' is not a string' |
823
|
|
|
Field 'key' with value 'array ( |
824
|
|
|
)' failed filtering, message 'Value 'array ( |
825
|
|
|
)' is not a string' |
826
|
|
|
Field 'key' with value 'NULL' failed filtering, message 'Value failed filtering, \$allowNull is set to false' |
827
|
|
|
Value at position '3' was not an array |
828
|
|
|
TXT; |
829
|
|
|
$this->assertSame($expected, $e->getMessage()); |
830
|
|
|
} |
831
|
|
|
} |
832
|
|
|
|
833
|
|
|
/** |
834
|
|
|
* @test |
835
|
|
|
* @covers ::ofArray |
836
|
|
|
*/ |
837
|
|
|
public function ofArray() |
838
|
|
|
{ |
839
|
|
|
$expected = ['key1' => 1, 'key2' => 2]; |
840
|
|
|
$spec = ['key1' => [['uint']], 'key2' => [['uint']]]; |
841
|
|
|
$this->assertSame($expected, Filterer::ofArray(['key1' => '1', 'key2' => '2'], $spec)); |
842
|
|
|
} |
843
|
|
|
|
844
|
|
|
/** |
845
|
|
|
* @test |
846
|
|
|
* @covers ::ofArray |
847
|
|
|
*/ |
848
|
|
|
public function ofArrayChained() |
849
|
|
|
{ |
850
|
|
|
$expected = ['key' => 3.3]; |
851
|
|
|
$spec = ['key' => [['trim', 'a'], ['floatval']]]; |
852
|
|
|
$this->assertSame($expected, Filterer::ofArray(['key' => 'a3.3'], $spec)); |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
/** |
856
|
|
|
* @test |
857
|
|
|
* @covers ::ofArray |
858
|
|
|
*/ |
859
|
|
|
public function ofArrayRequiredSuccess() |
860
|
|
|
{ |
861
|
|
|
$expected = ['key2' => 2]; |
862
|
|
|
$spec = ['key1' => [['uint']], 'key2' => ['required' => true, ['uint']]]; |
863
|
|
|
$this->assertSame($expected, Filterer::ofArray(['key2' => '2'], $spec)); |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
/** |
867
|
|
|
* @test |
868
|
|
|
* @covers ::ofArray |
869
|
|
|
*/ |
870
|
|
View Code Duplication |
public function ofArrayRequiredFail() |
|
|
|
|
871
|
|
|
{ |
872
|
|
|
try { |
873
|
|
|
Filterer::ofArray(['key1' => '1'], ['key1' => [['uint']], 'key2' => ['required' => true, ['uint']]]); |
874
|
|
|
$this->fail(); |
875
|
|
|
} catch (FilterException $e) { |
876
|
|
|
$expected = "Field 'key2' was required and not present"; |
877
|
|
|
$this->assertSame($expected, $e->getMessage()); |
878
|
|
|
} |
879
|
|
|
} |
880
|
|
|
|
881
|
|
|
/** |
882
|
|
|
* @test |
883
|
|
|
* @covers ::ofArray |
884
|
|
|
*/ |
885
|
|
View Code Duplication |
public function ofArrayUnknown() |
|
|
|
|
886
|
|
|
{ |
887
|
|
|
try { |
888
|
|
|
Filterer::ofArray(['key' => '1'], ['key2' => [['uint']]]); |
889
|
|
|
$this->fail(); |
890
|
|
|
} catch (FilterException $e) { |
891
|
|
|
$expected = "Field 'key' with value '1' is unknown"; |
892
|
|
|
$this->assertSame($expected, $e->getMessage()); |
893
|
|
|
} |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
/** |
897
|
|
|
* @test |
898
|
|
|
* @covers ::getAliases |
899
|
|
|
*/ |
900
|
|
|
public function getAliases() |
901
|
|
|
{ |
902
|
|
|
$expected = ['some' => 'alias']; |
903
|
|
|
|
904
|
|
|
$filterer = new Filterer([], [], $expected); |
905
|
|
|
$actual = $filterer->getAliases(); |
906
|
|
|
|
907
|
|
|
$this->assertSame($expected, $actual); |
908
|
|
|
} |
909
|
|
|
|
910
|
|
|
/** |
911
|
|
|
* @test |
912
|
|
|
* @covers ::getAliases |
913
|
|
|
*/ |
914
|
|
|
public function getAliasesReturnsStaticValueIfNull() |
915
|
|
|
{ |
916
|
|
|
$filterer = new Filterer([]); |
917
|
|
|
$actual = $filterer->getAliases(); |
918
|
|
|
|
919
|
|
|
$this->assertSame(Filterer::getFilterAliases(), $actual); |
920
|
|
|
} |
921
|
|
|
|
922
|
|
|
/** |
923
|
|
|
* @test |
924
|
|
|
* @covers ::getSpecification |
925
|
|
|
*/ |
926
|
|
|
public function getSpecification() |
927
|
|
|
{ |
928
|
|
|
$expected = ['some' => 'specification']; |
929
|
|
|
|
930
|
|
|
$filterer = new Filterer($expected); |
931
|
|
|
$actual = $filterer->getSpecification(); |
932
|
|
|
|
933
|
|
|
$this->assertSame($expected, $actual); |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
/** |
937
|
|
|
* @test |
938
|
|
|
* @covers ::withAliases |
939
|
|
|
*/ |
940
|
|
View Code Duplication |
public function withAliases() |
|
|
|
|
941
|
|
|
{ |
942
|
|
|
$expected = ['foo' => 'bar']; |
943
|
|
|
|
944
|
|
|
$filterer = new Filterer([]); |
945
|
|
|
$filtererCopy = $filterer->withAliases($expected); |
946
|
|
|
|
947
|
|
|
$this->assertNotSame($filterer, $filtererCopy); |
948
|
|
|
$this->assertSame($expected, $filtererCopy->getAliases()); |
949
|
|
|
} |
950
|
|
|
|
951
|
|
|
/** |
952
|
|
|
* @test |
953
|
|
|
* @covers ::withSpecification |
954
|
|
|
*/ |
955
|
|
View Code Duplication |
public function withSpecification() |
|
|
|
|
956
|
|
|
{ |
957
|
|
|
$expected = ['foo' => 'bar']; |
958
|
|
|
|
959
|
|
|
$filterer = new Filterer([]); |
960
|
|
|
$filtererCopy = $filterer->withSpecification($expected); |
961
|
|
|
|
962
|
|
|
$this->assertNotSame($filterer, $filtererCopy); |
963
|
|
|
$this->assertSame($expected, $filtererCopy->getSpecification()); |
964
|
|
|
} |
965
|
|
|
} |
966
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.