FiltererTest   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 1145
Duplicated Lines 0 %

Importance

Changes 33
Bugs 0 Features 0
Metric Value
wmc 50
eloc 590
c 33
b 0
f 0
dl 0
loc 1145
rs 8.4

44 Methods

Rating   Name   Duplication   Size   Complexity  
A filter() 0 3 1
A setUp() 0 3 1
A executeThrowsOnError() 0 17 1
A getSpecification() 0 8 1
A filterGetSetKnownFilters() 0 5 1
A ofScalarsFail() 0 15 2
A filterNotArray() 0 5 1
A getAliasesReturnsStaticValueIfNull() 0 6 1
A failingFilter() 0 3 1
B provideValidFilterData() 0 518 1
A getAliases() 0 8 1
A ofArrays() 0 4 1
A ofArraysRequiredAndUnknown() 0 8 2
A allowUnknownsNotBool() 0 5 1
A ofArraysFail() 0 20 2
A ofScalarsChained() 0 3 1
A ofScalars() 0 3 1
A ofArrayUnknown() 0 8 2
A filtersNotArrayInLeftOverSpec() 0 5 1
A notCallable() 0 5 1
A registerExistingAliasOverwriteFalse() 0 7 1
A setBadFilterAliases() 0 13 2
A ofArray() 0 5 1
A filterReturnsResponseType() 0 14 1
A filterCustomShortNamePass() 0 5 1
A registerAliasAliasNotString() 0 5 1
A filterWithNonStringError() 0 7 1
A withSpecification() 0 9 1
A executeValidatesReturnOnNull() 0 14 1
A requiredNotBool() 0 5 1
A registerExistingAliasOverwriteTrue() 0 5 1
A ofArrayRequiredFail() 0 8 2
A defaultRequiredNotBool() 0 5 1
A withAliases() 0 9 1
A ofScalarsWithMeaninglessKeys() 0 3 1
A filterWithEmptyStringError() 0 7 1
A ofArrayRequiredSuccess() 0 5 1
A passingFilter() 0 3 1
A filterThrowsExceptionOnInvalidResponseType() 0 6 1
A executeValidatesThrowsOnError() 0 14 1
A filterReturnsResponseTypeWithErrors() 0 14 1
A filtersNotArrayWithInput() 0 5 1
A ofArrayChained() 0 5 1
A ofArraysChained() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like FiltererTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FiltererTest, and based on these observations, apply Extract Interface, too.

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 TraderInteractive\Filter\Arrays;
13
use TypeError;
14
15
/**
16
 * @coversDefaultClass \TraderInteractive\Filterer
17
 * @covers ::__construct
18
 * @covers ::<private>
19
 */
20
final class FiltererTest extends TestCase
21
{
22
    /**
23
     * @var string
24
     */
25
    const FULL_XML = (''
26
        . "<?xml version=\"1.0\"?>\n"
27
            . '<books>'
28
                . '<book id="bk101">'
29
                    . '<author>Gambardella, Matthew</author>'
30
                    . "<title>XML Developer's Guide</title>"
31
                    . '<genre>Computer</genre>'
32
                    . '<price>44.95</price>'
33
                    . '<publish_date>2000-10-01</publish_date>'
34
                    . '<description>An in-depth look at creating applications with XML.</description>'
35
            . '</book>'
36
        . '<book id="bk102">'
37
                    . '<author>Ralls, Kim</author>'
38
                    . '<title>Midnight Rain</title>'
39
                    . '<genre>Fantasy</genre>'
40
                    . '<price>5.95</price>'
41
                    . '<publish_date>2000-12-16</publish_date>'
42
                    . '<description>A former architect battles corporate zombies</description>'
43
            . '</book>'
44
        . "</books>\n"
45
    );
46
47
    public function setUp(): void
48
    {
49
        Filterer::setFilterAliases(Filterer::DEFAULT_FILTER_ALIASES);
50
    }
51
52
    /**
53
     * @test
54
     * @covers ::filter
55
     * @covers ::execute
56
     * @dataProvider provideValidFilterData
57
     *
58
     * @param array $spec  The filter specification to be use.
59
     * @param array $input The input to be filtered.
60
     * @param array $options The filterer options
61
     * @param array $expectedResult The expected filterer result.
62
     */
63
    public function filter(array $spec, array $input, array $options, array $expectedResult)
64
    {
65
        $this->assertSame($expectedResult, Filterer::filter($spec, $input, $options));
66
    }
67
68
    public function provideValidFilterData() : array
69
    {
70
        return [
71
            'not required field not present' => [
72
                'spec' => ['fieldOne' => ['required' => false]],
73
                'input' => [],
74
                'options' => [],
75
                'result' => [true, [], null, []],
76
            ],
77
            'Required With A Default Without Input' => [
78
                'spec' => ['fieldOne' => ['required' => true, 'default' => 'theDefault']],
79
                'input' => [],
80
                'options' => [],
81
                'result' => [true, ['fieldOne' => 'theDefault'], null, []],
82
            ],
83
            'Required With A Null Default Without Input' => [
84
                'spec' => ['fieldOne' => ['required' => true, 'default' => null]],
85
                'input' => [],
86
                'options' => [],
87
                'result' => [true, ['fieldOne' => null], null, []],
88
            ],
89
            'Required With A Default With Input' => [
90
                'spec' => ['fieldOne' => ['required' => true, 'default' => 'theDefault']],
91
                'input' => ['fieldOne' => 'notTheDefault'],
92
                'options' => [],
93
                'result' => [true, ['fieldOne' => 'notTheDefault'], null, []],
94
            ],
95
            'Not Required With A Default Without Input' => [
96
                'spec' => ['fieldOne' => ['default' => 'theDefault']],
97
                'input' => [],
98
                'options' => [],
99
                'result' => [true, ['fieldOne' => 'theDefault'], null, []],
100
            ],
101
            'Not Required With A Default With Input' => [
102
                'spec' => ['fieldOne' => ['default' => 'theDefault']],
103
                'input' => ['fieldOne' => 'notTheDefault'],
104
                'options' => [],
105
                'result' => [true, ['fieldOne' => 'notTheDefault'], null, []],
106
            ],
107
            'Required Fail' => [
108
                'spec' => ['fieldOne' => ['required' => true]],
109
                'input' => [],
110
                'options' => [],
111
                'result' => [false, null, "Field 'fieldOne' was required and not present", []],
112
            ],
113
            'Required Default Pass' => [
114
                'spec' => ['fieldOne' => []],
115
                'input' => [],
116
                'options' => [],
117
                'result' => [true, [], null, []],
118
            ],
119
            'requiredDefaultFail' => [
120
                'spec' => ['fieldOne' => []],
121
                'input' => [],
122
                'options' => ['defaultRequired' => true],
123
                'result' => [false, null, "Field 'fieldOne' was required and not present", []],
124
            ],
125
            'filterPass' => [
126
                'spec' => ['fieldOne' => [['floatval']]],
127
                'input' => ['fieldOne' => '3.14'],
128
                'options' => [],
129
                'result' => [true, ['fieldOne' => 3.14], null, []],
130
            ],
131
            'filterDefaultShortNamePass' => [
132
                'spec' => ['fieldOne' => [['float']]],
133
                'input' => ['fieldOne' => '3.14'],
134
                'options' => [],
135
                'result' => [true, ['fieldOne' => 3.14], null, []],
136
            ],
137
            'filterFail' => [
138
                'spec' => ['fieldOne' => [['\TraderInteractive\FiltererTest::failingFilter']]],
139
                'input' => ['fieldOne' => 'valueOne'],
140
                'options' => [],
141
                'result' => [
142
                    false,
143
                    null,
144
                    "Field 'fieldOne' with value 'valueOne' failed filtering, message 'i failed'",
145
                    [],
146
                ],
147
            ],
148
            'chainPass' => [
149
                'spec' => ['fieldOne' => [['trim', 'a'], ['floatval']]],
150
                'input' => ['fieldOne' => 'a3.14'],
151
                'options' => [],
152
                'result' => [true, ['fieldOne' => 3.14], null, []],
153
            ],
154
            'chainFail' => [
155
                'spec' => ['fieldOne' => [['trim'], ['\TraderInteractive\FiltererTest::failingFilter']]],
156
                'input' => ['fieldOne' => 'the value'],
157
                'options' => [],
158
                'result' => [
159
                    false,
160
                    null,
161
                    "Field 'fieldOne' with value 'the value' failed filtering, message 'i failed'",
162
                    [],
163
                ],
164
            ],
165
            'multiInputPass' => [
166
                'spec' => ['fieldOne' => [['trim']], 'fieldTwo' => [['strtoupper']]],
167
                'input' => ['fieldOne' => ' value', 'fieldTwo' => 'bob'],
168
                'options' => [],
169
                'result' => [true, ['fieldOne' => 'value', 'fieldTwo' => 'BOB'], null, []],
170
            ],
171
            'multiInputFail' => [
172
                'spec' => [
173
                    'fieldOne' => [['\TraderInteractive\FiltererTest::failingFilter']],
174
                    'fieldTwo' => [['\TraderInteractive\FiltererTest::failingFilter']],
175
                ],
176
                'input' => ['fieldOne' => 'value one', 'fieldTwo' => 'value two'],
177
                'options' => [],
178
                'result' => [
179
                    false,
180
                    null,
181
                    "Field 'fieldOne' with value 'value one' failed filtering, message 'i failed'\n"
182
                    . "Field 'fieldTwo' with value 'value two' failed filtering, message 'i failed'",
183
                    [],
184
                ],
185
            ],
186
            'emptyFilter' => [
187
                'spec' => ['fieldOne' => [[]]],
188
                'input' => ['fieldOne' => 0],
189
                'options' => [],
190
                'result' => [true, ['fieldOne' => 0], null, []],
191
            ],
192
            'unknownsAllowed' => [
193
                'spec' => [],
194
                'input'=> ['fieldTwo' => 0],
195
                'options' => ['allowUnknowns' => true],
196
                'result' => [true, [], null, ['fieldTwo' => 0]],
197
            ],
198
            'unknownsNotAllowed' => [
199
                'spec' => [],
200
                'input' => ['fieldTwo' => 0],
201
                'options' => [],
202
                'result' => [false, null, "Field 'fieldTwo' with value '0' is unknown", ['fieldTwo' => 0]],
203
            ],
204
            'objectFilter' => [
205
                'spec' => ['fieldOne' => [[[$this, 'passingFilter']]]],
206
                'input' => ['fieldOne' => 'foo'],
207
                'options' => [],
208
                'result' => [true, ['fieldOne' => 'fooboo'], null, []],
209
            ],
210
            'filterWithCustomError' => [
211
                'spec' => [
212
                    'fieldOne' => [
213
                        'error' => 'My custom error message',
214
                        ['\TraderInteractive\FiltererTest::failingFilter'],
215
                    ],
216
                ],
217
                'input' => ['fieldOne' => 'valueOne'],
218
                'options' => [],
219
                'result' => [false, null, 'My custom error message', []],
220
            ],
221
            'filterWithCustomErrorContainingValuePlaceholder' => [
222
                'spec' => [
223
                    'fieldOne' => [
224
                        'error' => "The value '{value}' is invalid.",
225
                        ['uint'],
226
                    ],
227
                ],
228
                'input' => ['fieldOne' => 'abc'],
229
                'options' => [],
230
                'result' => [false, null, "The value 'abc' is invalid.", []],
231
            ],
232
            'arrayizeAliasIsCalledProperly' => [
233
                'spec' => ['field' => [['arrayize']]],
234
                'input' => ['field' => 'a string value'],
235
                'options' => [],
236
                'result' => [true, ['field' => ['a string value']], null, []],
237
            ],
238
            'concatAliasIsCalledProperly' => [
239
                'spec' => ['field' => [['concat', '%', '%']]],
240
                'input' => ['field' => 'value'],
241
                'options' => [],
242
                'result' => [true, ['field' => '%value%'], null, []],
243
            ],
244
            'translate alias' => [
245
                'spec' => ['field' => [['translate', ['active' => 'A', 'inactive' => 'I']]]],
246
                'input' => ['field' => 'inactive'],
247
                'options' => [],
248
                'result' => [true, ['field' => 'I'], null, []],
249
            ],
250
            'redact alias' => [
251
                'spec' => ['field' => [['redact', ['other'], '*']]],
252
                'input' => ['field' => 'one or other'],
253
                'options' => [],
254
                'result' => [true, ['field' => 'one or *****'], null, []],
255
            ],
256
            'compress-string alias' => [
257
                'spec' => ['field' => [['compress-string']]],
258
                'input' => ['field' => ' a  string    with    extra spaces '],
259
                'options' => [],
260
                'result' => [true, ['field' => 'a string with extra spaces'], null, []],
261
            ],
262
            'compress-string alias include newlines' => [
263
                'spec' => ['field' => [['compress-string', true]]],
264
                'input' => ['field' => " a  string\n    with\nnewlines  and    extra spaces\n "],
265
                'options' => [],
266
                'result' => [true, ['field' => 'a string with newlines and extra spaces'], null, []],
267
            ],
268
            'conflicts with single' => [
269
                'spec' => [
270
                    'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']],
271
                    'fieldTwo' => [['string']],
272
                    'fieldThree' => [FilterOptions::CONFLICTS_WITH => 'fieldOne', ['string']],
273
                ],
274
                'input' => [
275
                    'fieldOne' => 'abc',
276
                    'fieldTwo' => '123',
277
                    'fieldThree' => 'xyz',
278
                ],
279
                'options' => [],
280
                'result' => [
281
                    false,
282
                    null,
283
                    "Field 'fieldOne' cannot be given if field 'fieldThree' is present.\n"
284
                    . "Field 'fieldThree' cannot be given if field 'fieldOne' is present.",
285
                    [],
286
                ],
287
            ],
288
            'conflicts with multiple' => [
289
                'spec' => [
290
                    'fieldOne' => [FilterOptions::CONFLICTS_WITH => ['fieldTwo', 'fieldThree'], ['string']],
291
                    'fieldTwo' => [['string']],
292
                    'fieldThree' => [['string']],
293
                ],
294
                'input' => [
295
                    'fieldOne' => 'abc',
296
                    'fieldTwo' => '123',
297
                ],
298
                'options' => [],
299
                'result' => [
300
                    false,
301
                    null,
302
                    "Field 'fieldOne' cannot be given if field 'fieldTwo' is present.",
303
                    [],
304
                ],
305
            ],
306
            'conflicts with not present' => [
307
                'spec' => [
308
                    'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']],
309
                    'fieldTwo' => [['string']],
310
                ],
311
                'input' => [
312
                    'fieldOne' => 'abc',
313
                    'fieldTwo' => '123',
314
                ],
315
                'options' => [],
316
                'result' => [
317
                    true,
318
                    [
319
                        'fieldOne' => 'abc',
320
                        'fieldTwo' => '123',
321
                    ],
322
                    null,
323
                    [],
324
                ],
325
            ],
326
            'uses' => [
327
                'spec' => [
328
                    'fieldOne' => [['uint']],
329
                    'fieldTwo' => [
330
                        ['uint'],
331
                        [
332
                            FilterOptions::USES => ['fieldOne'],
333
                            function (int $input, int $fieldOneValue) : int {
334
                                return $input * $fieldOneValue;
335
                            },
336
                        ],
337
                    ],
338
                ],
339
                'input' => [
340
                    'fieldOne' => '5',
341
                    'fieldTwo' => '2',
342
                ],
343
                'options' => [],
344
                'result' => [
345
                    true,
346
                    [
347
                        'fieldOne' => 5,
348
                        'fieldTwo' => 10,
349
                    ],
350
                    null,
351
                    [],
352
                ],
353
            ],
354
            'input order does not matter for uses' => [
355
                'spec' => [
356
                    'fieldOne' => [['uint']],
357
                    'fieldTwo' => [
358
                        ['uint'],
359
                        [
360
                            FilterOptions::USES => ['fieldOne'],
361
                            function (int $input, int $fieldOneValue) : int {
362
                                return $input * $fieldOneValue;
363
                            },
364
                        ],
365
                    ],
366
                ],
367
                'input' => [
368
                    'fieldTwo' => '2',
369
                    'fieldOne' => '5',
370
                ],
371
                'options' => [],
372
                'result' => [
373
                    true,
374
                    [
375
                        'fieldOne' => 5,
376
                        'fieldTwo' => 10,
377
                    ],
378
                    null,
379
                    [],
380
                ],
381
            ],
382
            'uses missing field' => [
383
                'spec' => [
384
                    'fieldOne' => [['uint']],
385
                    'fieldTwo' => [
386
                        ['uint'],
387
                        [
388
                            FilterOptions::USES => ['fieldOne'],
389
                            function (int $input, int $fieldOneValue) : int {
390
                                return $input * $fieldOneValue;
391
                            },
392
                        ],
393
                    ],
394
                ],
395
                'input' => [
396
                    'fieldTwo' => '2',
397
                ],
398
                'options' => [],
399
                'result' => [
400
                    false,
401
                    null,
402
                    "Field 'fieldTwo' with value '2' failed filtering, message 'fieldTwo uses fieldOne but fieldOne was"
403
                    . " not given.'",
404
                    [],
405
                ],
406
            ],
407
            'returnOnNull filter option' => [
408
                'spec' => ['field' => [FilterOptions::RETURN_ON_NULL => true, ['string', true], ['string']]],
409
                'input' => ['field' => null],
410
                'options' => [],
411
                'result' => [true, ['field' => null], null, []],
412
            ],
413
            'phone alias' => [
414
                'spec' => ['field' => [['phone']]],
415
                'input' => ['field' => '(234) 567 8901'],
416
                'options' => [],
417
                'result' => [true, ['field' => '2345678901'], null, []],
418
            ],
419
            'json alias' => [
420
                'spec' => ['field' => [['json']]],
421
                'input' => ['field' => '{"foo": "bar"}'],
422
                'options' => [],
423
                'result' => [true, ['field' => '{"foo": "bar"}'], null, []],
424
            ],
425
            'json-decode alias' => [
426
                'spec' => ['field' => [['json-decode']]],
427
                'input' => ['field' => '{"foo": "bar"}'],
428
                'options' => [],
429
                'result' => [true, ['field' => ['foo' => 'bar']], null, []],
430
            ],
431
            'xml alias' => [
432
                'spec' => ['field' => [['xml']]],
433
                'input' => ['field' => self::FULL_XML],
434
                'options' => [],
435
                'result' => [true, ['field' => self::FULL_XML], null, []],
436
            ],
437
            'xml-validate alias' => [
438
                'spec' => ['field' => [['xml-validate', __DIR__ . '/_files/books.xsd']]],
439
                'input' => ['field' => self::FULL_XML],
440
                'options' => [],
441
                'result' => [true, ['field' => self::FULL_XML], null, []],
442
            ],
443
            'xml-extract alias' => [
444
                'spec' => ['field' => [['xml-extract', "/books/book[@id='bk101']"]]],
445
                'input' => ['field' => self::FULL_XML],
446
                'options' => [],
447
                'result' => [
448
                    true,
449
                    [
450
                        'field' => (''
451
                            . '<book id="bk101">'
452
                                . '<author>Gambardella, Matthew</author>'
453
                                . "<title>XML Developer's Guide</title>"
454
                                . '<genre>Computer</genre>'
455
                                . '<price>44.95</price>'
456
                                . '<publish_date>2000-10-01</publish_date>'
457
                                . '<description>An in-depth look at creating applications with XML.</description>'
458
                            . '</book>'
459
                        ),
460
                    ],
461
                    null,
462
                    []
463
                ],
464
            ],
465
            'array-copy' => [
466
                'spec' => [
467
                    'field' => [['array-copy', ['FOO_VALUE' => 'foo', 'BAR_VALUE' => 'bar']]],
468
                ],
469
                'input' => ['field' => ['foo' => 'abc', 'bar' => 123]],
470
                'options' => [],
471
                'result' => [
472
                    true,
473
                    ['field' => ['FOO_VALUE' => 'abc', 'BAR_VALUE' => 123]],
474
                    null,
475
                    [],
476
                ],
477
            ],
478
            'array-copy-each' => [
479
                'spec' => [
480
                    'field' => [['array-copy-each', ['FOO_VALUE' => 'foo', 'BAR_VALUE' => 'bar']]],
481
                ],
482
                'input' => [
483
                    'field' => [
484
                        ['foo' => 'abc', 'bar' => 123],
485
                        ['foo' => 'xyz', 'bar' => 789],
486
                    ],
487
                ],
488
                'options' => [],
489
                'result' => [
490
                    true,
491
                    [
492
                        'field' => [
493
                            ['FOO_VALUE' => 'abc', 'BAR_VALUE' => 123],
494
                            ['FOO_VALUE' => 'xyz', 'BAR_VALUE' => 789],
495
                        ],
496
                    ],
497
                    null,
498
                    [],
499
                ],
500
            ],
501
            'array-pad' => [
502
                'spec' => [
503
                    'field' => [['array-pad', 5, 0, Arrays::ARRAY_PAD_FRONT]],
504
                ],
505
                'input' => [
506
                    'field' => ['a', 'b', 'c'],
507
                ],
508
                'options' => [],
509
                'result' => [
510
                    true,
511
                    [
512
                        'field' => [0, 0, 'a', 'b', 'c'],
513
                    ],
514
                    null,
515
                    [],
516
                ],
517
            ],
518
            'time-of-day' => [
519
                'spec' => [
520
                    'field' => [['time-of-day']],
521
                ],
522
                'input' => [
523
                    'field' => '23:59:59',
524
                ],
525
                'options' => [],
526
                'result' => [
527
                    true,
528
                    [
529
                        'field' => '23:59:59',
530
                    ],
531
                    null,
532
                    [],
533
                ],
534
            ],
535
            'implode' => [
536
                'spec' => [
537
                    'field' => [['array'], ['implode', ',']],
538
                ],
539
                'input' => [
540
                    'field' => ['one', 'two', 'three'],
541
                ],
542
                'options' => [],
543
                'result' => [
544
                    true,
545
                    [
546
                        'field' => 'one,two,three',
547
                    ],
548
                    null,
549
                    [],
550
                ],
551
            ],
552
            'uuid' => [
553
                'spec' => [
554
                    'field' => [['uuid', false, false, [4]]],
555
                ],
556
                'input' => [
557
                    'field' => '2c02b87a-97ec-4de0-8c50-6721a29c150f',
558
                ],
559
                'options' => [],
560
                'result' => [
561
                    true,
562
                    [
563
                        'field' => '2c02b87a-97ec-4de0-8c50-6721a29c150f',
564
                    ],
565
                    null,
566
                    [],
567
                ],
568
            ],
569
            'strip-emoji' => [
570
                'spec' => [
571
                    'field' => [['strip-emoji']],
572
                ],
573
                'input' => [
574
                    'field' => 'This 💩 text contains 😞 multiple emoji 🍔 characters 🍚. As well as an alphanumeric '
575
                    . 'supplement 🆗 and flag 🚩',
576
                ],
577
                'options' => [],
578
                'result' => [
579
                    true,
580
                    [
581
                        'field' => 'This  text contains  multiple emoji  characters . As well as an alphanumeric '
582
                        . 'supplement  and flag ',
583
                    ],
584
                    null,
585
                    [],
586
                ],
587
            ],
588
        ];
589
    }
590
591
    /**
592
     * @test
593
     * @covers ::execute
594
     */
595
    public function executeThrowsOnError()
596
    {
597
        $exception = new RuntimeException('the error');
598
        $this->expectException(RuntimeException::class);
599
        $this->expectExceptionMessage($exception->getMessage());
600
        $filter = function () use ($exception) {
601
            throw $exception;
602
        };
603
604
        $specification = [
605
            'id' => [
606
                FilterOptions::THROW_ON_ERROR => true,
607
                [$filter],
608
            ],
609
        ];
610
        $filterer = new Filterer($specification);
611
        $filterer->execute(['id' => 1]);
612
    }
613
614
    /**
615
     * @test
616
     * @covers ::execute
617
     */
618
    public function executeValidatesThrowsOnError()
619
    {
620
        $this->expectException(InvalidArgumentException::class);
621
        $this->expectExceptionMessage(
622
            sprintf(Filterer::INVALID_BOOLEAN_FILTER_OPTION, FilterOptions::THROW_ON_ERROR, 'id')
623
        );
624
        $specification = [
625
            'id' => [
626
                FilterOptions::THROW_ON_ERROR => 'abc',
627
                ['uint'],
628
            ],
629
        ];
630
        $filterer = new Filterer($specification);
631
        $filterer->execute(['id' => 1]);
632
    }
633
634
    /**
635
     * @test
636
     * @covers ::execute
637
     */
638
    public function executeValidatesReturnOnNull()
639
    {
640
        $this->expectException(InvalidArgumentException::class);
641
        $this->expectExceptionMessage(
642
            sprintf(Filterer::INVALID_BOOLEAN_FILTER_OPTION, FilterOptions::RETURN_ON_NULL, 'id')
643
        );
644
        $specification = [
645
            'id' => [
646
                FilterOptions::RETURN_ON_NULL => 'abc',
647
                ['uint'],
648
            ],
649
        ];
650
        $filterer = new Filterer($specification);
651
        $filterer->execute(['id' => 1]);
652
    }
653
654
    /**
655
     * @test
656
     * @covers ::filter
657
     * @covers ::execute
658
     */
659
    public function filterReturnsResponseType()
660
    {
661
        $specification = ['id' => [['uint']]];
662
        $input = ['id' => 1];
663
        $options = ['responseType' => Filterer::RESPONSE_TYPE_FILTER];
664
665
        $result = Filterer::filter($specification, $input, $options);
666
667
        $this->assertInstanceOf(FilterResponse::class, $result);
668
        $this->assertSame(true, $result->success);
669
        $this->assertSame($input, $result->filteredValue);
670
        $this->assertSame([], $result->errors);
671
        $this->assertSame(null, $result->errorMessage);
672
        $this->assertSame([], $result->unknowns);
673
    }
674
675
    /**
676
     * @test
677
     * @covers ::filter
678
     * @covers ::execute
679
     */
680
    public function filterReturnsResponseTypeWithErrors()
681
    {
682
        $specification = ['name' => [['string']]];
683
        $input = ['name' => 'foo', 'id' => 1];
684
        $options = ['responseType' => Filterer::RESPONSE_TYPE_FILTER];
685
686
        $result = Filterer::filter($specification, $input, $options);
687
688
        $this->assertInstanceOf(FilterResponse::class, $result);
689
        $this->assertSame(false, $result->success);
690
        $this->assertSame(['name' => 'foo'], $result->filteredValue);
691
        $this->assertSame(['id' => "Field 'id' with value '1' is unknown"], $result->errors);
692
        $this->assertSame("Field 'id' with value '1' is unknown", $result->errorMessage);
693
        $this->assertSame(['id' => 1], $result->unknowns);
694
    }
695
696
    /**
697
     * @test
698
     * @covers ::filter
699
     * @covers ::execute
700
     * @covers ::setFilterAliases
701
     */
702
    public function filterCustomShortNamePass()
703
    {
704
        Filterer::setFilterAliases(['fval' => 'floatval']);
705
        $result = Filterer::filter(['fieldOne' => [['fval']]], ['fieldOne' => '3.14']);
706
        $this->assertSame([true, ['fieldOne' => 3.14], null, []], $result);
707
    }
708
709
    /**
710
     * @test
711
     * @covers ::filter
712
     * @covers ::execute
713
     * @covers ::setFilterAliases
714
     * @covers ::getFilterAliases
715
     */
716
    public function filterGetSetKnownFilters()
717
    {
718
        $knownFilters = ['lower' => 'strtolower', 'upper' => 'strtoupper'];
719
        Filterer::setFilterAliases($knownFilters);
720
        $this->assertSame($knownFilters, Filterer::getFilterAliases());
721
    }
722
723
    /**
724
     * @test
725
     * @covers ::setFilterAliases
726
     */
727
    public function setBadFilterAliases()
728
    {
729
        $originalAliases = Filterer::getFilterAliases();
730
731
        $actualThrowable = null;
732
        try {
733
            Filterer::setFilterAliases(['foo' => 'not callable']);
734
        } catch (Throwable $throwable) {
735
            $actualThrowable = $throwable;
736
        }
737
738
        $this->assertSame($originalAliases, Filterer::getFilterAliases());
739
        $this->assertInstanceOf(TypeError::class, $actualThrowable);
740
    }
741
742
    /**
743
     * @test
744
     * @covers ::filter
745
     * @covers ::execute
746
     */
747
    public function notCallable()
748
    {
749
        $this->expectException(Exception::class);
750
        $this->expectExceptionMessage("Function 'boo' for field 'foo' is not callable");
751
        Filterer::filter(['foo' => [['boo']]], ['foo' => 0]);
752
    }
753
754
    /**
755
     * @test
756
     * @covers ::filter
757
     * @covers ::execute
758
     */
759
    public function allowUnknownsNotBool()
760
    {
761
        $this->expectException(InvalidArgumentException::class);
762
        $this->expectExceptionMessage("'allowUnknowns' option was not a bool");
763
        Filterer::filter([], [], ['allowUnknowns' => 1]);
764
    }
765
766
    /**
767
     * @test
768
     * @covers ::filter
769
     * @covers ::execute
770
     */
771
    public function defaultRequiredNotBool()
772
    {
773
        $this->expectException(InvalidArgumentException::class);
774
        $this->expectExceptionMessage("'defaultRequired' option was not a bool");
775
        Filterer::filter([], [], ['defaultRequired' => 1]);
776
    }
777
778
    /**
779
     * @test
780
     * @covers ::filter
781
     * @covers ::execute
782
     */
783
    public function filterThrowsExceptionOnInvalidResponseType()
784
    {
785
        $this->expectException(InvalidArgumentException::class);
786
        $this->expectExceptionMessage("'responseType' was not a recognized value");
787
788
        Filterer::filter([], [], ['responseType' => 'invalid']);
789
    }
790
791
    /**
792
     * @test
793
     * @covers ::filter
794
     * @covers ::execute
795
     */
796
    public function filtersNotArrayInLeftOverSpec()
797
    {
798
        $this->expectException(InvalidArgumentException::class);
799
        $this->expectExceptionMessage("filters for field 'boo' was not a array");
800
        Filterer::filter(['boo' => 1], []);
801
    }
802
803
    /**
804
     * @test
805
     * @covers ::filter
806
     * @covers ::execute
807
     */
808
    public function filtersNotArrayWithInput()
809
    {
810
        $this->expectException(InvalidArgumentException::class);
811
        $this->expectExceptionMessage("filters for field 'boo' was not a array");
812
        Filterer::filter(['boo' => 1], ['boo' => 'notUnderTest']);
813
    }
814
815
    /**
816
     * @test
817
     * @covers ::filter
818
     * @covers ::execute
819
     */
820
    public function filterNotArray()
821
    {
822
        $this->expectException(InvalidArgumentException::class);
823
        $this->expectExceptionMessage("filter for field 'boo' was not a array");
824
        Filterer::filter(['boo' => [1]], ['boo' => 'notUnderTest']);
825
    }
826
827
    /**
828
     * @test
829
     * @covers ::filter
830
     * @covers ::execute
831
     */
832
    public function requiredNotBool()
833
    {
834
        $this->expectException(InvalidArgumentException::class);
835
        $this->expectExceptionMessage("'required' for field 'boo' was not a bool");
836
        Filterer::filter(['boo' => ['required' => 1]], []);
837
    }
838
839
    /**
840
     * @test
841
     * @covers ::registerAlias
842
     */
843
    public function registerAliasAliasNotString()
844
    {
845
        $this->expectException(InvalidArgumentException::class);
846
        $this->expectExceptionMessage('$alias was not a string or int');
847
        Filterer::registerAlias(true, 'strtolower');
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type integer|string expected by parameter $alias of TraderInteractive\Filterer::registerAlias(). ( Ignorable by Annotation )

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

847
        Filterer::registerAlias(/** @scrutinizer ignore-type */ true, 'strtolower');
Loading history...
848
    }
849
850
    /**
851
     * @test
852
     * @covers ::registerAlias
853
     */
854
    public function registerExistingAliasOverwriteFalse()
855
    {
856
        $this->expectException(Exception::class);
857
        $this->expectExceptionMessage("Alias 'upper' exists");
858
        Filterer::setFilterAliases([]);
859
        Filterer::registerAlias('upper', 'strtoupper');
860
        Filterer::registerAlias('upper', 'strtoupper', false);
861
    }
862
863
    /**
864
     * @test
865
     * @covers ::registerAlias
866
     */
867
    public function registerExistingAliasOverwriteTrue()
868
    {
869
        Filterer::setFilterAliases(['upper' => 'strtoupper', 'lower' => 'strtolower']);
870
        Filterer::registerAlias('upper', 'ucfirst', true);
871
        $this->assertSame(['upper' => 'ucfirst', 'lower' => 'strtolower'], Filterer::getFilterAliases());
872
    }
873
874
    public static function failingFilter()
875
    {
876
        throw new Exception('i failed');
877
    }
878
879
    public static function passingFilter($value)
880
    {
881
        return $value . 'boo';
882
    }
883
884
    /**
885
     * Verify behavior of filter() when 'error' is not a string value.
886
     *
887
     * @test
888
     * @covers ::filter
889
     * @covers ::execute
890
     *
891
     * @return void
892
     */
893
    public function filterWithNonStringError()
894
    {
895
        $this->expectException(InvalidArgumentException::class);
896
        $this->expectExceptionMessage("error for field 'fieldOne' was not a non-empty string");
897
        Filterer::filter(
898
            ['fieldOne' => [['strtoupper'], 'error' => new stdClass()]],
899
            ['fieldOne' => 'valueOne']
900
        );
901
    }
902
903
    /**
904
     * Verify behavior of filter() when 'error' is an empty string.
905
     *
906
     * @test
907
     * @covers ::filter
908
     * @covers ::execute
909
     *
910
     * @return void
911
     */
912
    public function filterWithEmptyStringError()
913
    {
914
        $this->expectException(InvalidArgumentException::class);
915
        $this->expectExceptionMessage("error for field 'fieldOne' was not a non-empty string");
916
        Filterer::filter(
917
            ['fieldOne' => [['strtoupper'], 'error' => "\n   \t"]],
918
            ['fieldOne' => 'valueOne']
919
        );
920
    }
921
922
    /**
923
     * @test
924
     * @covers ::ofScalars
925
     */
926
    public function ofScalars()
927
    {
928
        $this->assertSame([1, 2], Filterer::ofScalars(['1', '2'], [['uint']]));
929
    }
930
931
    /**
932
     * @test
933
     * @covers ::ofScalars
934
     */
935
    public function ofScalarsChained()
936
    {
937
        $this->assertSame([3.3, 5.5], Filterer::ofScalars(['a3.3', 'a5.5'], [['trim', 'a'], ['floatval']]));
938
    }
939
940
    /**
941
     * @test
942
     * @covers ::ofScalars
943
     */
944
    public function ofScalarsWithMeaninglessKeys()
945
    {
946
        $this->assertSame(['key1' => 1, 'key2' => 2], Filterer::ofScalars(['key1' => '1', 'key2' => '2'], [['uint']]));
947
    }
948
949
    /**
950
     * @test
951
     * @covers ::ofScalars
952
     */
953
    public function ofScalarsFail()
954
    {
955
        try {
956
            Filterer::ofScalars(['1', [], new stdClass], [['string']]);
957
            $this->fail();
958
        } catch (FilterException $e) {
959
            $expected = <<<TXT
960
Field '1' with value 'array (
961
)' failed filtering, message 'Value 'array (
962
)' is not a string'
963
Field '2' with value '(object) array(
964
)' failed filtering, message 'Value '(object) array(
965
)' is not a string'
966
TXT;
967
            $this->assertSame($expected, $e->getMessage());
968
        }
969
    }
970
971
    /**
972
     * @test
973
     * @covers ::ofArrays
974
     */
975
    public function ofArrays()
976
    {
977
        $expected = [['key' => 1], ['key' => 2]];
978
        $this->assertSame($expected, Filterer::ofArrays([['key' => '1'], ['key' => '2']], ['key' => [['uint']]]));
979
    }
980
981
    /**
982
     * @test
983
     * @covers ::ofArrays
984
     */
985
    public function ofArraysChained()
986
    {
987
        $expected = [['key' => 3.3], ['key' => 5.5]];
988
        $spec = ['key' => [['trim', 'a'], ['floatval']]];
989
        $this->assertSame($expected, Filterer::ofArrays([['key' => 'a3.3'], ['key' => 'a5.5']], $spec));
990
    }
991
992
    /**
993
     * @test
994
     * @covers ::ofArrays
995
     */
996
    public function ofArraysRequiredAndUnknown()
997
    {
998
        try {
999
            Filterer::ofArrays([['key' => '1'], ['key2' => '2']], ['key' => ['required' => true, ['uint']]]);
1000
            $this->fail();
1001
        } catch (Exception $e) {
1002
            $expected = "Field 'key' was required and not present\nField 'key2' with value '2' is unknown";
1003
            $this->assertSame($expected, $e->getMessage());
1004
        }
1005
    }
1006
1007
    /**
1008
     * @test
1009
     * @covers ::ofArrays
1010
     */
1011
    public function ofArraysFail()
1012
    {
1013
        try {
1014
            Filterer::ofArrays(
1015
                [['key' => new stdClass], ['key' => []], ['key' => null], 'key'],
1016
                ['key' => [['string']]]
1017
            );
1018
            $this->fail();
1019
        } catch (FilterException $e) {
1020
            $expected = <<<TXT
1021
Field 'key' with value '(object) array(
1022
)' failed filtering, message 'Value '(object) array(
1023
)' is not a string'
1024
Field 'key' with value 'array (
1025
)' failed filtering, message 'Value 'array (
1026
)' is not a string'
1027
Field 'key' with value 'NULL' failed filtering, message 'Value failed filtering, \$allowNull is set to false'
1028
Value at position '3' was not an array
1029
TXT;
1030
            $this->assertSame($expected, $e->getMessage());
1031
        }
1032
    }
1033
1034
    /**
1035
     * @test
1036
     * @covers ::ofArray
1037
     */
1038
    public function ofArray()
1039
    {
1040
        $expected = ['key1' => 1, 'key2' => 2];
1041
        $spec = ['key1' => [['uint']], 'key2' => [['uint']]];
1042
        $this->assertSame($expected, Filterer::ofArray(['key1' => '1', 'key2' => '2'], $spec));
1043
    }
1044
1045
    /**
1046
     * @test
1047
     * @covers ::ofArray
1048
     */
1049
    public function ofArrayChained()
1050
    {
1051
        $expected = ['key' => 3.3];
1052
        $spec = ['key' => [['trim', 'a'], ['floatval']]];
1053
        $this->assertSame($expected, Filterer::ofArray(['key' => 'a3.3'], $spec));
1054
    }
1055
1056
    /**
1057
     * @test
1058
     * @covers ::ofArray
1059
     */
1060
    public function ofArrayRequiredSuccess()
1061
    {
1062
        $expected = ['key2' => 2];
1063
        $spec = ['key1' => [['uint']], 'key2' => ['required' => true, ['uint']]];
1064
        $this->assertSame($expected, Filterer::ofArray(['key2' => '2'], $spec));
1065
    }
1066
1067
    /**
1068
     * @test
1069
     * @covers ::ofArray
1070
     */
1071
    public function ofArrayRequiredFail()
1072
    {
1073
        try {
1074
            Filterer::ofArray(['key1' => '1'], ['key1' => [['uint']], 'key2' => ['required' => true, ['uint']]]);
1075
            $this->fail();
1076
        } catch (FilterException $e) {
1077
            $expected = "Field 'key2' was required and not present";
1078
            $this->assertSame($expected, $e->getMessage());
1079
        }
1080
    }
1081
1082
    /**
1083
     * @test
1084
     * @covers ::ofArray
1085
     */
1086
    public function ofArrayUnknown()
1087
    {
1088
        try {
1089
            Filterer::ofArray(['key' => '1'], ['key2' => [['uint']]]);
1090
            $this->fail();
1091
        } catch (FilterException $e) {
1092
            $expected = "Field 'key' with value '1' is unknown";
1093
            $this->assertSame($expected, $e->getMessage());
1094
        }
1095
    }
1096
1097
    /**
1098
     * @test
1099
     * @covers ::getAliases
1100
     */
1101
    public function getAliases()
1102
    {
1103
        $expected = ['some' => 'alias'];
1104
1105
        $filterer = new Filterer([], [], $expected);
1106
        $actual = $filterer->getAliases();
1107
1108
        $this->assertSame($expected, $actual);
1109
    }
1110
1111
    /**
1112
     * @test
1113
     * @covers ::getAliases
1114
     */
1115
    public function getAliasesReturnsStaticValueIfNull()
1116
    {
1117
        $filterer = new Filterer([]);
1118
        $actual = $filterer->getAliases();
1119
1120
        $this->assertSame(Filterer::getFilterAliases(), $actual);
1121
    }
1122
1123
    /**
1124
     * @test
1125
     * @covers ::getSpecification
1126
     */
1127
    public function getSpecification()
1128
    {
1129
        $expected = ['some' => 'specification'];
1130
1131
        $filterer = new Filterer($expected);
1132
        $actual = $filterer->getSpecification();
1133
1134
        $this->assertSame($expected, $actual);
1135
    }
1136
1137
    /**
1138
     * @test
1139
     * @covers ::withAliases
1140
     */
1141
    public function withAliases()
1142
    {
1143
        $expected = ['foo' => 'bar'];
1144
1145
        $filterer = new Filterer([]);
1146
        $filtererCopy = $filterer->withAliases($expected);
1147
1148
        $this->assertNotSame($filterer, $filtererCopy);
1149
        $this->assertSame($expected, $filtererCopy->getAliases());
1150
    }
1151
1152
    /**
1153
     * @test
1154
     * @covers ::withSpecification
1155
     */
1156
    public function withSpecification()
1157
    {
1158
        $expected = ['foo' => 'bar'];
1159
1160
        $filterer = new Filterer([]);
1161
        $filtererCopy = $filterer->withSpecification($expected);
1162
1163
        $this->assertNotSame($filterer, $filtererCopy);
1164
        $this->assertSame($expected, $filtererCopy->getSpecification());
1165
    }
1166
}
1167