Completed
Pull Request — master (#81)
by Chad
01:16
created

FiltererTest::filter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 4
1
<?php
2
3
namespace TraderInteractive;
4
5
use Exception;
6
use InvalidArgumentException;
7
use PHPUnit\Framework\TestCase;
8
use StdClass;
9
use TraderInteractive\Exceptions\FilterException;
10
11
/**
12
 * @coversDefaultClass \TraderInteractive\Filterer
13
 * @covers ::<private>
14
 */
15
final class FiltererTest extends TestCase
16
{
17
    public function setUp()
18
    {
19
        Filterer::setFilterAliases(Filterer::DEFAULT_FILTER_ALIASES);
20
    }
21
22
    /**
23
     * @test
24
     * @covers ::filter
25
     * @dataProvider provideValidFilterData
26
     *
27
     * @param array $spec  The filter specification to be use.
28
     * @param array $input The input to be filtered.
29
     * @param array $options The filterer options
30
     * @param array $expectedResult The expected filterer result.
31
     */
32
    public function filter(array $spec, array $input, array $options, array $expectedResult)
33
    {
34
        $this->assertSame($expectedResult, Filterer::filter($spec, $input, $options));
35
    }
36
37
    public function provideValidFilterData() : array
38
    {
39
        return [
40
            'not required field not present' => [
41
                'spec' => ['fieldOne' => ['required' => false]],
42
                'input' => [],
43
                'options' => [],
44
                'result' => [true, [], null, []],
45
            ],
46
            'Required With A Default Without Input' => [
47
                'spec' => ['fieldOne' => ['required' => true, 'default' => 'theDefault']],
48
                'input' => [],
49
                'options' => [],
50
                'result' => [true, ['fieldOne' => 'theDefault'], null, []],
51
            ],
52
            'Required With A Null Default Without Input' => [
53
                'spec' => ['fieldOne' => ['required' => true, 'default' => null]],
54
                'input' => [],
55
                'options' => [],
56
                'result' => [true, ['fieldOne' => null], null, []],
57
            ],
58
            'Required With A Default With Input' => [
59
                'spec' => ['fieldOne' => ['required' => true, 'default' => 'theDefault']],
60
                'input' => ['fieldOne' => 'notTheDefault'],
61
                'options' => [],
62
                'result' => [true, ['fieldOne' => 'notTheDefault'], null, []],
63
            ],
64
            'Not Required With A Default Without Input' => [
65
                'spec' => ['fieldOne' => ['default' => 'theDefault']],
66
                'input' => [],
67
                'options' => [],
68
                'result' => [true, ['fieldOne' => 'theDefault'], null, []],
69
            ],
70
            'Not Required With A Default With Input' => [
71
                'spec' => ['fieldOne' => ['default' => 'theDefault']],
72
                'input' => ['fieldOne' => 'notTheDefault'],
73
                'options' => [],
74
                'result' => [true, ['fieldOne' => 'notTheDefault'], null, []],
75
            ],
76
            'Required Fail' => [
77
                'spec' => ['fieldOne' => ['required' => true]],
78
                'input' => [],
79
                'options' => [],
80
                'result' => [false, null, "Field 'fieldOne' was required and not present", []],
81
            ],
82
            'Required Default Pass' => [
83
                'spec' => ['fieldOne' => []],
84
                'input' => [],
85
                'options' => [],
86
                'result' => [true, [], null, []],
87
            ],
88
            'requiredDefaultFail' => [
89
                'spec' => ['fieldOne' => []],
90
                'input' => [],
91
                'options' => ['defaultRequired' => true],
92
                'result' => [false, null, "Field 'fieldOne' was required and not present", []],
93
            ],
94
            'filterPass' => [
95
                'spec' => ['fieldOne' => [['floatval']]],
96
                'input' => ['fieldOne' => '3.14'],
97
                'options' => [],
98
                'result' => [true, ['fieldOne' => 3.14], null, []],
99
            ],
100
            'filterDefaultShortNamePass' => [
101
                'spec' => ['fieldOne' => [['float']]],
102
                'input' => ['fieldOne' => '3.14'],
103
                'options' => [],
104
                'result' => [true, ['fieldOne' => 3.14], null, []],
105
            ],
106
            'filterFail' => [
107
                'spec' => ['fieldOne' => [['\TraderInteractive\FiltererTest::failingFilter']]],
108
                'input' => ['fieldOne' => 'valueOne'],
109
                'options' => [],
110
                'result' => [
111
                    false,
112
                    null,
113
                    "Field 'fieldOne' with value 'valueOne' failed filtering, message 'i failed'",
114
                    [],
115
                ],
116
            ],
117
            'chainPass' => [
118
                'spec' => ['fieldOne' => [['trim', 'a'], ['floatval']]],
119
                'input' => ['fieldOne' => 'a3.14'],
120
                'options' => [],
121
                'result' => [true, ['fieldOne' => 3.14], null, []],
122
            ],
123
            'chainFail' => [
124
                'spec' => ['fieldOne' => [['trim'], ['\TraderInteractive\FiltererTest::failingFilter']]],
125
                'input' => ['fieldOne' => 'the value'],
126
                'options' => [],
127
                'result' => [
128
                    false,
129
                    null,
130
                    "Field 'fieldOne' with value 'the value' failed filtering, message 'i failed'",
131
                    [],
132
                ],
133
            ],
134
            'multiInputPass' => [
135
                'spec' => ['fieldOne' => [['trim']], 'fieldTwo' => [['strtoupper']]],
136
                'input' => ['fieldOne' => ' value', 'fieldTwo' => 'bob'],
137
                'options' => [],
138
                'result' => [true, ['fieldOne' => 'value', 'fieldTwo' => 'BOB'], null, []],
139
            ],
140
            'multiInputFail' => [
141
                'spec' => [
142
                    'fieldOne' => [['\TraderInteractive\FiltererTest::failingFilter']],
143
                    'fieldTwo' => [['\TraderInteractive\FiltererTest::failingFilter']],
144
                ],
145
                'input' => ['fieldOne' => 'value one', 'fieldTwo' => 'value two'],
146
                'options' => [],
147
                'result' => [
148
                    false,
149
                    null,
150
                    "Field 'fieldOne' with value 'value one' failed filtering, message 'i failed'\n"
151
                    . "Field 'fieldTwo' with value 'value two' failed filtering, message 'i failed'",
152
                    [],
153
                ],
154
            ],
155
            'emptyFilter' => [
156
                'spec' => ['fieldOne' => [[]]],
157
                'input' => ['fieldOne' => 0],
158
                'options' => [],
159
                'result' => [true, ['fieldOne' => 0], null, []],
160
            ],
161
            'unknownsAllowed' => [
162
                'spec' => [],
163
                'input'=> ['fieldTwo' => 0],
164
                'options' => ['allowUnknowns' => true],
165
                'result' => [true, [], null, ['fieldTwo' => 0]],
166
            ],
167
            'unknownsNotAllowed' => [
168
                'spec' => [],
169
                'input' => ['fieldTwo' => 0],
170
                'options' => [],
171
                'result' => [false, null, "Field 'fieldTwo' with value '0' is unknown", ['fieldTwo' => 0]],
172
            ],
173
            'objectFilter' => [
174
                'spec' => ['fieldOne' => [[[$this, 'passingFilter']]]],
175
                'input' => ['fieldOne' => 'foo'],
176
                'options' => [],
177
                'result' => [true, ['fieldOne' => 'fooboo'], null, []],
178
            ],
179
            'filterWithCustomError' => [
180
                'spec' => [
181
                    'fieldOne' => [
182
                        'error' => 'My custom error message',
183
                        ['\TraderInteractive\FiltererTest::failingFilter'],
184
                    ],
185
                ],
186
                'input' => ['fieldOne' => 'valueOne'],
187
                'options' => [],
188
                'result' => [false, null, 'My custom error message', []],
189
            ],
190
            'filterWithCustomErrorContainingValuePlaceholder' => [
191
                'spec' => [
192
                    'fieldOne' => [
193
                        'error' => "The value '{value}' is invalid.",
194
                        ['uint'],
195
                    ],
196
                ],
197
                'input' => ['fieldOne' => 'abc'],
198
                'options' => [],
199
                'result' => [false, null, "The value 'abc' is invalid.", []],
200
            ],
201
            'arrayizeAliasIsCalledProperly' => [
202
                'spec' => ['field' => [['arrayize']]],
203
                'input' => ['field' => 'a string value'],
204
                'options' => [],
205
                'result' => [true, ['field' => ['a string value']], null, []],
206
            ],
207
            'concatAliasIsCalledProperly' => [
208
                'spec' => ['field' => [['concat', '%', '%']]],
209
                'input' => ['field' => 'value'],
210
                'options' => [],
211
                'result' => [true, ['field' => '%value%'], null, []],
212
            ],
213
        ];
214
    }
215
216
    /**
217
     * @test
218
     * @covers ::filter
219
     * @covers ::setFilterAliases
220
     */
221
    public function filterCustomShortNamePass()
222
    {
223
        Filterer::setFilterAliases(['fval' => 'floatval']);
224
        $result = Filterer::filter(['fieldOne' => [['fval']]], ['fieldOne' => '3.14']);
225
        $this->assertSame([true, ['fieldOne' => 3.14], null, []], $result);
226
    }
227
228
    /**
229
     * @test
230
     * @covers ::filter
231
     * @covers ::setFilterAliases
232
     * @covers ::getFilterAliases
233
     */
234
    public function filterGetSetKnownFilters()
235
    {
236
        $knownFilters = ['lower' => 'strtolower', 'upper' => 'strtoupper'];
237
        Filterer::setFilterAliases($knownFilters);
238
        $this->assertSame($knownFilters, Filterer::getFilterAliases());
239
    }
240
241
    /**
242
     * @test
243
     * @covers ::filter
244
     * @expectedException Exception
245
     * @expectedExceptionMessage Function 'boo' for field 'foo' is not callable
246
     */
247
    public function notCallable()
248
    {
249
        Filterer::filter(['foo' => [['boo']]], ['foo' => 0]);
250
    }
251
252
    /**
253
     * @test
254
     * @covers ::filter
255
     * @expectedException InvalidArgumentException
256
     * @expectedExceptionMessage 'allowUnknowns' option was not a bool
257
     */
258
    public function allowUnknownsNotBool()
259
    {
260
        Filterer::filter([], [], ['allowUnknowns' => 1]);
261
    }
262
263
    /**
264
     * @test
265
     * @covers ::filter
266
     * @expectedException InvalidArgumentException
267
     * @expectedExceptionMessage 'defaultRequired' option was not a bool
268
     */
269
    public function defaultRequiredNotBool()
270
    {
271
        Filterer::filter([], [], ['defaultRequired' => 1]);
272
    }
273
274
    /**
275
     * @test
276
     * @covers ::filter
277
     * @expectedException InvalidArgumentException
278
     * @expectedExceptionMessage filters for field 'boo' was not a array
279
     */
280
    public function filtersNotArrayInLeftOverSpec()
281
    {
282
        Filterer::filter(['boo' => 1], []);
283
    }
284
285
    /**
286
     * @test
287
     * @covers ::filter
288
     * @expectedException InvalidArgumentException
289
     * @expectedExceptionMessage filters for field 'boo' was not a array
290
     */
291
    public function filtersNotArrayWithInput()
292
    {
293
        Filterer::filter(['boo' => 1], ['boo' => 'notUnderTest']);
294
    }
295
296
    /**
297
     * @test
298
     * @covers ::filter
299
     * @expectedException InvalidArgumentException
300
     * @expectedExceptionMessage filter for field 'boo' was not a array
301
     */
302
    public function filterNotArray()
303
    {
304
        Filterer::filter(['boo' => [1]], ['boo' => 'notUnderTest']);
305
    }
306
307
    /**
308
     * @test
309
     * @covers ::filter
310
     * @expectedException InvalidArgumentException
311
     * @expectedExceptionMessage 'required' for field 'boo' was not a bool
312
     */
313
    public function requiredNotBool()
314
    {
315
        Filterer::filter(['boo' => ['required' => 1]], []);
316
    }
317
318
    /**
319
     * @test
320
     * @covers ::registerAlias
321
     * @expectedException InvalidArgumentException
322
     * @expectedExceptionMessage $alias was not a string or int
323
     */
324
    public function registerAliasAliasNotString()
325
    {
326
        Filterer::registerAlias(true, 'strtolower');
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
327
    }
328
329
    /**
330
     * @test
331
     * @covers ::registerAlias
332
     * @expectedException Exception
333
     * @expectedExceptionMessage Alias 'upper' exists
334
     */
335
    public function registerExistingAliasOverwriteFalse()
336
    {
337
        Filterer::setFilterAliases([]);
338
        Filterer::registerAlias('upper', 'strtoupper');
339
        Filterer::registerAlias('upper', 'strtoupper', false);
340
    }
341
342
    /**
343
     * @test
344
     * @covers ::registerAlias
345
     */
346
    public function registerExistingAliasOverwriteTrue()
347
    {
348
        Filterer::setFilterAliases(['upper' => 'strtoupper', 'lower' => 'strtolower']);
349
        Filterer::registerAlias('upper', 'ucfirst', true);
350
        $this->assertSame(['upper' => 'ucfirst', 'lower' => 'strtolower'], Filterer::getFilterAliases());
351
    }
352
353
    public static function failingFilter()
354
    {
355
        throw new Exception('i failed');
356
    }
357
358
    public static function passingFilter($value)
359
    {
360
        return $value . 'boo';
361
    }
362
363
    /**
364
     * Verify behavior of filter() when 'error' is not a string value.
365
     *
366
     * @test
367
     * @covers ::filter
368
     * @expectedException InvalidArgumentException
369
     * @expectedExceptionMessage error for field 'fieldOne' was not a non-empty string
370
     *
371
     * @return void
372
     */
373 View Code Duplication
    public function filterWithNonStringError()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
374
    {
375
        Filterer::filter(
376
            ['fieldOne' => [['strtoupper'], 'error' => new StdClass()]],
377
            ['fieldOne' => 'valueOne']
378
        );
379
    }
380
381
    /**
382
     * Verify behavior of filter() when 'error' is an empty string.
383
     *
384
     * @test
385
     * @covers ::filter
386
     * @expectedException InvalidArgumentException
387
     * @expectedExceptionMessage error for field 'fieldOne' was not a non-empty string
388
     *
389
     * @return void
390
     */
391 View Code Duplication
    public function filterWithEmptyStringError()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
392
    {
393
        Filterer::filter(
394
            ['fieldOne' => [['strtoupper'], 'error' => "\n   \t"]],
395
            ['fieldOne' => 'valueOne']
396
        );
397
    }
398
399
    /**
400
     * @test
401
     * @covers ::ofScalars
402
     */
403
    public function ofScalars()
404
    {
405
        $this->assertSame([1, 2], Filterer::ofScalars(['1', '2'], [['uint']]));
406
    }
407
408
    /**
409
     * @test
410
     * @covers ::ofScalars
411
     */
412
    public function ofScalarsChained()
413
    {
414
        $this->assertSame([3.3, 5.5], Filterer::ofScalars(['a3.3', 'a5.5'], [['trim', 'a'], ['floatval']]));
415
    }
416
417
    /**
418
     * @test
419
     * @covers ::ofScalars
420
     */
421
    public function ofScalarsWithMeaninglessKeys()
422
    {
423
        $this->assertSame(['key1' => 1, 'key2' => 2], Filterer::ofScalars(['key1' => '1', 'key2' => '2'], [['uint']]));
424
    }
425
426
    /**
427
     * @test
428
     * @covers ::ofScalars
429
     */
430 View Code Duplication
    public function ofScalarsFail()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
431
    {
432
        try {
433
            Filterer::ofScalars(['1', [], new \StdClass], [['string']]);
434
            $this->fail();
435
        } catch (FilterException $e) {
436
            $expected = <<<TXT
437
Field '1' with value 'array (
438
)' failed filtering, message 'Value 'array (
439
)' is not a string'
440
Field '2' with value 'stdClass::__set_state(array(
441
))' failed filtering, message 'Value 'stdClass::__set_state(array(
442
))' is not a string'
443
TXT;
444
            $this->assertSame($expected, $e->getMessage());
445
        }
446
    }
447
448
    /**
449
     * @test
450
     * @covers ::ofArrays
451
     */
452
    public function ofArrays()
453
    {
454
        $expected = [['key' => 1], ['key' => 2]];
455
        $this->assertSame($expected, Filterer::ofArrays([['key' => '1'], ['key' => '2']], ['key' => [['uint']]]));
456
    }
457
458
    /**
459
     * @test
460
     * @covers ::ofArrays
461
     */
462
    public function ofArraysChained()
463
    {
464
        $expected = [['key' => 3.3], ['key' => 5.5]];
465
        $spec = ['key' => [['trim', 'a'], ['floatval']]];
466
        $this->assertSame($expected, Filterer::ofArrays([['key' => 'a3.3'], ['key' => 'a5.5']], $spec));
467
    }
468
469
    /**
470
     * @test
471
     * @covers ::ofArrays
472
     */
473 View Code Duplication
    public function ofArraysRequiredAndUnknown()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
474
    {
475
        try {
476
            Filterer::ofArrays([['key' => '1'], ['key2' => '2']], ['key' => ['required' => true, ['uint']]]);
477
            $this->fail();
478
        } catch (Exception $e) {
479
            $expected = "Field 'key' was required and not present\nField 'key2' with value '2' is unknown";
480
            $this->assertSame($expected, $e->getMessage());
481
        }
482
    }
483
484
    /**
485
     * @test
486
     * @covers ::ofArrays
487
     */
488
    public function ofArraysFail()
489
    {
490
        try {
491
            Filterer::ofArrays(
492
                [['key' => new \StdClass], ['key' => []], ['key' => null], 'key'],
493
                ['key' => [['string']]]
494
            );
495
            $this->fail();
496
        } catch (FilterException $e) {
497
            $expected = <<<TXT
498
Field 'key' with value 'stdClass::__set_state(array(
499
))' failed filtering, message 'Value 'stdClass::__set_state(array(
500
))' is not a string'
501
Field 'key' with value 'array (
502
)' failed filtering, message 'Value 'array (
503
)' is not a string'
504
Field 'key' with value 'NULL' failed filtering, message 'Value failed filtering, \$allowNull is set to false'
505
Value at position '3' was not an array
506
TXT;
507
            $this->assertSame($expected, $e->getMessage());
508
        }
509
    }
510
511
    /**
512
     * @test
513
     * @covers ::ofArray
514
     */
515
    public function ofArray()
516
    {
517
        $expected = ['key1' => 1, 'key2' => 2];
518
        $spec = ['key1' => [['uint']], 'key2' => [['uint']]];
519
        $this->assertSame($expected, Filterer::ofArray(['key1' => '1', 'key2' => '2'], $spec));
520
    }
521
522
    /**
523
     * @test
524
     * @covers ::ofArray
525
     */
526
    public function ofArrayChained()
527
    {
528
        $expected = ['key' => 3.3];
529
        $spec = ['key' => [['trim', 'a'], ['floatval']]];
530
        $this->assertSame($expected, Filterer::ofArray(['key' => 'a3.3'], $spec));
531
    }
532
533
    /**
534
     * @test
535
     * @covers ::ofArray
536
     */
537
    public function ofArrayRequiredSuccess()
538
    {
539
        $expected = ['key2' => 2];
540
        $spec = ['key1' => [['uint']], 'key2' => ['required' => true, ['uint']]];
541
        $this->assertSame($expected, Filterer::ofArray(['key2' => '2'], $spec));
542
    }
543
544
    /**
545
     * @test
546
     * @covers ::ofArray
547
     */
548 View Code Duplication
    public function ofArrayRequiredFail()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
549
    {
550
        try {
551
            Filterer::ofArray(['key1' => '1'], ['key1' => [['uint']], 'key2' => ['required' => true, ['uint']]]);
552
            $this->fail();
553
        } catch (FilterException $e) {
554
            $expected = "Field 'key2' was required and not present";
555
            $this->assertSame($expected, $e->getMessage());
556
        }
557
    }
558
559
    /**
560
     * @test
561
     * @covers ::ofArray
562
     */
563 View Code Duplication
    public function ofArrayUnknown()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
564
    {
565
        try {
566
            Filterer::ofArray(['key' => '1'], ['key2' => [['uint']]]);
567
            $this->fail();
568
        } catch (FilterException $e) {
569
            $expected = "Field 'key' with value '1' is unknown";
570
            $this->assertSame($expected, $e->getMessage());
571
        }
572
    }
573
}
574