1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace TraderInteractive\Filter; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use PHPUnit\Framework\TestCase; |
7
|
|
|
use TraderInteractive\Exceptions\FilterException; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* @coversDefaultClass \TraderInteractive\Filter\Strings |
11
|
|
|
* @covers ::<private> |
12
|
|
|
*/ |
13
|
|
|
final class StringsTest extends TestCase |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Verify basic use of filter |
17
|
|
|
* |
18
|
|
|
* @test |
19
|
|
|
* @covers ::filter |
20
|
|
|
* @dataProvider filterData |
21
|
|
|
* |
22
|
|
|
* @param mixed $input The input. |
23
|
|
|
* @param mixed $expected The expected value(s). |
24
|
|
|
* |
25
|
|
|
* @return void |
26
|
|
|
* @throws FilterException |
27
|
|
|
*/ |
28
|
|
|
public function filter($input, $expected) |
29
|
|
|
{ |
30
|
|
|
$this->assertSame($expected, Strings::filter($input)); |
31
|
|
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Data provider for basic filter tests |
35
|
|
|
* |
36
|
|
|
* @return array |
37
|
|
|
*/ |
38
|
|
|
public function filterData() |
39
|
|
|
{ |
40
|
|
|
return [ |
41
|
|
|
'string' => ['abc', 'abc'], |
42
|
|
|
'int' => [1, '1'], |
43
|
|
|
'float' => [1.1, '1.1'], |
44
|
|
|
'bool' => [true, '1'], |
45
|
|
|
'object' => [new \SplFileInfo(__FILE__), __FILE__], |
46
|
|
|
]; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @test |
51
|
|
|
* @covers ::filter |
52
|
|
|
*/ |
53
|
|
|
public function filterNullPass() |
54
|
|
|
{ |
55
|
|
|
$this->assertNull(Strings::filter(null, true)); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @test |
60
|
|
|
* @covers ::filter |
61
|
|
|
*/ |
62
|
|
|
public function filterNullFail() |
63
|
|
|
{ |
64
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
65
|
|
|
$this->expectExceptionMessage('Value failed filtering, $allowNull is set to false'); |
66
|
|
|
Strings::filter(null); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @test |
71
|
|
|
* @covers ::filter |
72
|
|
|
*/ |
73
|
|
|
public function filterMinLengthPass() |
74
|
|
|
{ |
75
|
|
|
$this->assertSame('a', Strings::filter('a')); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @test |
80
|
|
|
* @covers ::filter |
81
|
|
|
*/ |
82
|
|
|
public function filterMinLengthFail() |
83
|
|
|
{ |
84
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
85
|
|
|
Strings::filter(''); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @test |
90
|
|
|
* @covers ::filter |
91
|
|
|
*/ |
92
|
|
|
public function filterMaxLengthPass() |
93
|
|
|
{ |
94
|
|
|
$this->assertSame('a', Strings::filter('a', false, 0, 1)); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @test |
99
|
|
|
* @covers ::filter |
100
|
|
|
*/ |
101
|
|
|
public function filterMaxLengthFail() |
102
|
|
|
{ |
103
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
104
|
|
|
$this->expectExceptionMessage("Value 'a' with length '1' is less than '0' or greater than '0'"); |
105
|
|
|
Strings::filter('a', false, 0, 0); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @test |
110
|
|
|
* @covers ::filter |
111
|
|
|
*/ |
112
|
|
|
public function filterMinLengthNotInteger() |
113
|
|
|
{ |
114
|
|
|
$this->expectException(InvalidArgumentException::class); |
115
|
|
|
$this->expectExceptionMessage('$minLength was not a positive integer value'); |
116
|
|
|
Strings::filter('a', false, -1); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @test |
121
|
|
|
* @covers ::filter |
122
|
|
|
*/ |
123
|
|
|
public function filterMaxLengthNotInteger() |
124
|
|
|
{ |
125
|
|
|
$this->expectException(InvalidArgumentException::class); |
126
|
|
|
$this->expectExceptionMessage('$maxLength was not a positive integer value'); |
127
|
|
|
Strings::filter('a', false, 1, -1); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* @test |
132
|
|
|
* @covers ::filter |
133
|
|
|
*/ |
134
|
|
|
public function filterMinLengthNegative() |
135
|
|
|
{ |
136
|
|
|
$this->expectException(InvalidArgumentException::class); |
137
|
|
|
$this->expectExceptionMessage('$minLength was not a positive integer value'); |
138
|
|
|
Strings::filter('a', false, -1); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* @test |
143
|
|
|
* @covers ::filter |
144
|
|
|
*/ |
145
|
|
|
public function filterMaxLengthNegative() |
146
|
|
|
{ |
147
|
|
|
$this->expectException(InvalidArgumentException::class); |
148
|
|
|
$this->expectExceptionMessage('$maxLength was not a positive integer value'); |
149
|
|
|
Strings::filter('a', false, 1, -1); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* @test |
154
|
|
|
* @covers ::filter |
155
|
|
|
*/ |
156
|
|
|
public function filterWithScalar() |
157
|
|
|
{ |
158
|
|
|
$this->assertSame('24141', Strings::filter(24141)); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @test |
163
|
|
|
* @covers ::filter |
164
|
|
|
*/ |
165
|
|
|
public function filterWithObject() |
166
|
|
|
{ |
167
|
|
|
$testObject = new class() { |
168
|
|
|
private $data; |
169
|
|
|
|
170
|
|
|
public function __construct() |
171
|
|
|
{ |
172
|
|
|
$this->data = [1,2,3,4,5]; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
public function __toString() |
176
|
|
|
{ |
177
|
|
|
return implode(',', $this->data); |
178
|
|
|
} |
179
|
|
|
}; |
180
|
|
|
|
181
|
|
|
$this->assertSame('1,2,3,4,5', Strings::filter(new $testObject)); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @test |
186
|
|
|
* @covers ::filter |
187
|
|
|
*/ |
188
|
|
|
public function filterWithObjectNoToStringMethod() |
189
|
|
|
{ |
190
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
191
|
|
|
$this->expectExceptionMessageMatches("/Value '\\\\?class\@anonymous/"); |
192
|
|
|
$testObject = new class() { |
193
|
|
|
private $data; |
194
|
|
|
|
195
|
|
|
public function __construct() |
196
|
|
|
{ |
197
|
|
|
$this->data = [1, 2, 3, 4, 5]; |
198
|
|
|
} |
199
|
|
|
}; |
200
|
|
|
|
201
|
|
|
Strings::filter(new $testObject); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* @test |
206
|
|
|
* @covers ::translate |
207
|
|
|
*/ |
208
|
|
|
public function translateValue() |
209
|
|
|
{ |
210
|
|
|
$map = ['foo' => '100', 'bar' => '200']; |
211
|
|
|
$this->assertSame('100', Strings::translate('foo', $map)); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* @test |
216
|
|
|
* @covers ::translate |
217
|
|
|
*/ |
218
|
|
|
public function translateValueNotFoundInMap() |
219
|
|
|
{ |
220
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
221
|
|
|
$this->expectExceptionMessage("The value 'baz' was not found in the translation map array."); |
222
|
|
|
$map = ['foo' => '100', 'bar' => '200']; |
223
|
|
|
Strings::translate('baz', $map); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Verifies basic explode functionality. |
228
|
|
|
* |
229
|
|
|
* @test |
230
|
|
|
* @covers ::explode |
231
|
|
|
*/ |
232
|
|
|
public function explode() |
233
|
|
|
{ |
234
|
|
|
$this->assertSame(['a', 'bcd', 'e'], Strings::explode('a,bcd,e')); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Verifies explode with a custom delimiter. |
239
|
|
|
* |
240
|
|
|
* @test |
241
|
|
|
* @covers ::explode |
242
|
|
|
*/ |
243
|
|
|
public function explodeCustomDelimiter() |
244
|
|
|
{ |
245
|
|
|
$this->assertSame(['a', 'b', 'c', 'd,e'], Strings::explode('a b c d,e', ' ')); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* @test |
250
|
|
|
* @covers ::explode |
251
|
|
|
*/ |
252
|
|
|
public function explodeNonString() |
253
|
|
|
{ |
254
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
255
|
|
|
$this->expectExceptionMessage("Value '1234' is not a string"); |
256
|
|
|
Strings::explode(1234, ''); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Verifies explode filter with an empty delimiter. |
261
|
|
|
* |
262
|
|
|
* @test |
263
|
|
|
* @covers ::explode |
264
|
|
|
*/ |
265
|
|
|
public function explodeEmptyDelimiter() |
266
|
|
|
{ |
267
|
|
|
$this->expectException(\InvalidArgumentException::class); |
268
|
|
|
$this->expectExceptionMessage("Delimiter '''' is not a non-empty string"); |
269
|
|
|
Strings::explode('test', ''); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @test |
274
|
|
|
* @covers ::stripTags |
275
|
|
|
* @dataProvider provideStripTags |
276
|
|
|
* |
277
|
|
|
* @param string|null $value |
278
|
|
|
* @param string $replacement |
279
|
|
|
* @param string|null $expected |
280
|
|
|
*/ |
281
|
|
|
public function stripTags($value, string $replacement, $expected) |
282
|
|
|
{ |
283
|
|
|
$actual = Strings::stripTags($value, $replacement); |
284
|
|
|
$this->assertSame($expected, $actual); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* @return array |
289
|
|
|
*/ |
290
|
|
|
public function provideStripTags() |
291
|
|
|
{ |
292
|
|
|
return [ |
293
|
|
|
'null returns null' => [ |
294
|
|
|
'value' => null, |
295
|
|
|
'replacement' => '', |
296
|
|
|
'expected' => null, |
297
|
|
|
], |
298
|
|
|
'remove html from string' => [ |
299
|
|
|
'value' => 'A string with <p>paragraph</p> tags', |
300
|
|
|
'replacement' => '', |
301
|
|
|
'expected' => 'A string with paragraph tags', |
302
|
|
|
], |
303
|
|
|
'remove xml and replace with space' => [ |
304
|
|
|
'value' => '<something>inner value</something>', |
305
|
|
|
'replacement' => ' ', |
306
|
|
|
'expected' => ' inner value ', |
307
|
|
|
], |
308
|
|
|
'remove multiline html from string' => [ |
309
|
|
|
'value' => "<p\nclass='something'\nstyle='display:none'></p>", |
310
|
|
|
'replacement' => ' ', |
311
|
|
|
'expected' => ' ', |
312
|
|
|
], |
313
|
|
|
'remove php tags' => [ |
314
|
|
|
'value' => '<?php some php code', |
315
|
|
|
'replacement' => ' ', |
316
|
|
|
'expected' => '', |
317
|
|
|
], |
318
|
|
|
'remove shorthand php tags' => [ |
319
|
|
|
'value' => '<?= some php code ?> something else', |
320
|
|
|
'replacement' => ' ', |
321
|
|
|
'expected' => ' something else', |
322
|
|
|
], |
323
|
|
|
'do not remove unmatched <' => [ |
324
|
|
|
'value' => '1 < 3', |
325
|
|
|
'replacement' => ' ', |
326
|
|
|
'expected' => '1 < 3', |
327
|
|
|
], |
328
|
|
|
'do not remove unmatched >' => [ |
329
|
|
|
'value' => '3 > 1', |
330
|
|
|
'replacement' => ' ', |
331
|
|
|
'expected' => '3 > 1', |
332
|
|
|
], |
333
|
|
|
]; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* @test |
338
|
|
|
* @covers ::concat |
339
|
|
|
*/ |
340
|
|
|
public function concat() |
341
|
|
|
{ |
342
|
|
|
$this->assertSame('prefixstringsuffix', Strings::concat('string', 'prefix', 'suffix')); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Verify behavior of concat() when $value is not filterable |
347
|
|
|
* |
348
|
|
|
* @test |
349
|
|
|
* @covers ::concat |
350
|
|
|
* |
351
|
|
|
* @return void |
352
|
|
|
*/ |
353
|
|
|
public function concatValueNotFilterable() |
354
|
|
|
{ |
355
|
|
|
$this->expectException(\TraderInteractive\Exceptions\FilterException::class); |
356
|
|
|
Strings::concat(new \StdClass(), 'prefix', 'suffix'); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* @test |
361
|
|
|
* @covers ::concat |
362
|
|
|
*/ |
363
|
|
|
public function concatScalarValue() |
364
|
|
|
{ |
365
|
|
|
$this->assertSame('prefix123suffix', Strings::concat(123, 'prefix', 'suffix')); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* @test |
370
|
|
|
* @covers ::concat |
371
|
|
|
*/ |
372
|
|
|
public function concatObjectValue() |
373
|
|
|
{ |
374
|
|
|
$this->assertSame( |
375
|
|
|
'prefix' . __FILE__ . 'suffix', |
376
|
|
|
Strings::concat(new \SplFileInfo(__FILE__), 'prefix', 'suffix') |
377
|
|
|
); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* @test |
382
|
|
|
* @covers ::compress |
383
|
|
|
*/ |
384
|
|
|
public function compressRemovesSuperfluousWhitespace() |
385
|
|
|
{ |
386
|
|
|
$this->assertSame('a compressed string', Strings::compress(' a compressed string ')); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* @test |
391
|
|
|
* @covers ::compress |
392
|
|
|
*/ |
393
|
|
|
public function compressReturnsNullIfValueIsNull() |
394
|
|
|
{ |
395
|
|
|
$this->assertNull(Strings::compress(null)); |
|
|
|
|
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* @test |
400
|
|
|
* @covers ::compress |
401
|
|
|
*/ |
402
|
|
|
public function compressRemovesNewLines() |
403
|
|
|
{ |
404
|
|
|
$input = " This string\nhas superfluous whitespace and \nnewlines\n"; |
405
|
|
|
$this->assertSame( |
406
|
|
|
'This string has superfluous whitespace and newlines', |
407
|
|
|
Strings::compress($input, true) |
408
|
|
|
); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* @test |
413
|
|
|
* @covers ::compress |
414
|
|
|
*/ |
415
|
|
|
public function compressIgnoresNewLinesByDefault() |
416
|
|
|
{ |
417
|
|
|
$input = " This string\nhas superfluous whitespace and \nnewlines\n"; |
418
|
|
|
$this->assertSame( |
419
|
|
|
"This string\nhas superfluous whitespace and \nnewlines", |
420
|
|
|
Strings::compress($input) |
421
|
|
|
); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* @test |
426
|
|
|
* @covers ::redact |
427
|
|
|
* @dataProvider provideRedact |
428
|
|
|
* |
429
|
|
|
* @param string|null $value The value to pass to the filter. |
430
|
|
|
* @param array|callable $words The words to pass to the filter. |
431
|
|
|
* @param string $replacement The replacement to pass to the filter. |
432
|
|
|
* @param string|null $expected The expected result. |
433
|
|
|
*/ |
434
|
|
|
public function redact($value, $words, string $replacement, $expected) |
435
|
|
|
{ |
436
|
|
|
$actual = Strings::redact($value, $words, $replacement); |
437
|
|
|
|
438
|
|
|
$this->assertSame($expected, $actual); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* @return array |
443
|
|
|
*/ |
444
|
|
|
public function provideRedact() : array |
445
|
|
|
{ |
446
|
|
|
return [ |
447
|
|
|
'null value' => [ |
448
|
|
|
'value' => null, |
449
|
|
|
'words' => [], |
450
|
|
|
'replacement' => '', |
451
|
|
|
'expected' => null, |
452
|
|
|
], |
453
|
|
|
'empty string' => [ |
454
|
|
|
'value' => '', |
455
|
|
|
'words' => [], |
456
|
|
|
'replacement' => '', |
457
|
|
|
'expected' => '', |
458
|
|
|
], |
459
|
|
|
'replace with empty' => [ |
460
|
|
|
'value' => 'this message contains something that you want removed', |
461
|
|
|
'words' => ['something that you want removed'], |
462
|
|
|
'replacement' => '', |
463
|
|
|
'expected' => 'this message contains ', |
464
|
|
|
], |
465
|
|
|
'replace with *' => [ |
466
|
|
|
'value' => 'replace certain words that you might want to remove', |
467
|
|
|
'words' => ['might', 'certain'], |
468
|
|
|
'replacement' => '*', |
469
|
|
|
'expected' => 'replace ******* words that you ***** want to remove', |
470
|
|
|
], |
471
|
|
|
'replace with █' => [ |
472
|
|
|
'value' => 'redact specific dates and secret locations', |
473
|
|
|
'words' => ['secret locations', 'specific dates'], |
474
|
|
|
'replacement' => '█', |
475
|
|
|
'expected' => 'redact ██████████████ and ████████████████', |
476
|
|
|
], |
477
|
|
|
'replace with multi-character string uses first character' => [ |
478
|
|
|
'value' => 'replace some particular words', |
479
|
|
|
'words' => ['particular', 'words', 'some'], |
480
|
|
|
'replacement' => ' *** ', |
481
|
|
|
'expected' => 'replace ', |
482
|
|
|
], |
483
|
|
|
'no replacements' => [ |
484
|
|
|
'value' => 'some perfectly normal string', |
485
|
|
|
'words' => ['undesired', 'words'], |
486
|
|
|
'replacement' => '*', |
487
|
|
|
'expected' => 'some perfectly normal string', |
488
|
|
|
], |
489
|
|
|
'closure provides words' => [ |
490
|
|
|
'value' => 'doe a deer, a female deer', |
491
|
|
|
'words' => function () { |
492
|
|
|
return ['doe', 'deer']; |
493
|
|
|
}, |
494
|
|
|
'replacement' => '-', |
495
|
|
|
'expected' => '--- a ----, a female ----', |
496
|
|
|
], |
497
|
|
|
]; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* @test |
502
|
|
|
* @covers ::redact |
503
|
|
|
* @dataProvider provideRedactFailsOnBadInput |
504
|
|
|
* |
505
|
|
|
* @param mixed $value The value to pass to the filter. |
506
|
|
|
* @param mixed $words The words to pass to the filter. |
507
|
|
|
* @param string $replacement The replacement to pass to the filter. |
508
|
|
|
* @param string $exception The exception to expect. |
509
|
|
|
* @param string $message The exception message to expect. |
510
|
|
|
*/ |
511
|
|
|
public function redactFailsOnBadInput($value, $words, string $replacement, string $exception, string $message) |
512
|
|
|
{ |
513
|
|
|
$this->expectException($exception); |
514
|
|
|
$this->expectExceptionMessage($message); |
515
|
|
|
|
516
|
|
|
Strings::redact($value, $words, $replacement); |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
/** |
520
|
|
|
* @return array |
521
|
|
|
*/ |
522
|
|
|
public function provideRedactFailsOnBadInput() : array |
523
|
|
|
{ |
524
|
|
|
return [ |
525
|
|
|
'non-string value' => [ |
526
|
|
|
'value' => ['bad', 'input'], |
527
|
|
|
'words' => [], |
528
|
|
|
'replacement' => '', |
529
|
|
|
'exception' => FilterException::class, |
530
|
|
|
'message' => "Value '" . var_export(['bad', 'input'], true) . "' is not a string", |
531
|
|
|
], |
532
|
|
|
'invalid words argument' => [ |
533
|
|
|
'value' => 'some string', |
534
|
|
|
'words' => 'this is not valid', |
535
|
|
|
'replacement' => '', |
536
|
|
|
'exception' => FilterException::class, |
537
|
|
|
'message' => 'Words was not an array or a callable that returns an array', |
538
|
|
|
], |
539
|
|
|
'invalid return from callable words argument' => [ |
540
|
|
|
'value' => 'some string', |
541
|
|
|
'words' => function () { |
542
|
|
|
return 'this is also not valid'; |
543
|
|
|
}, |
544
|
|
|
'replacement' => '', |
545
|
|
|
'exception' => FilterException::class, |
546
|
|
|
'message' => 'Words was not an array or a callable that returns an array', |
547
|
|
|
], |
548
|
|
|
]; |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.