diff options
author | Brian Evans <grknight@gentoo.org> | 2019-04-11 11:08:13 -0400 |
---|---|---|
committer | Brian Evans <grknight@gentoo.org> | 2019-04-11 11:08:13 -0400 |
commit | e6f63b37820d165b55e4c9bf262b3d6d92e28c67 (patch) | |
tree | df5db2f24d45da64a8e3d90104cc5021dff3bf9c /AbuseFilter/tests | |
parent | Drop Flow extension (diff) | |
download | extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.tar.gz extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.tar.bz2 extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.zip |
Upgrade AbuseFilter to 1.32
Signed-off-by: Brian Evans <grknight@gentoo.org>
Diffstat (limited to 'AbuseFilter/tests')
85 files changed, 4187 insertions, 170 deletions
diff --git a/AbuseFilter/tests/parserTests/arith.t b/AbuseFilter/tests/parserTests/arith.t index e3c42e32..1f88e9df 100644 --- a/AbuseFilter/tests/parserTests/arith.t +++ b/AbuseFilter/tests/parserTests/arith.t @@ -1 +1,27 @@ -(1 + 1 == 2) & (5 - 3 = 2) & (2 * 3 = 6) & (10 / 2 = 5) & (10 % 7 = 3) & (2 ** 4 = 16) +(1 + 1 === 2) & +(1.5 + 1.5 === 3.0) & +(2.5 + 1 === 3.5) & +(0 + 1 === 1) & +(2.5 + 0 === 2.5) & +(5 - 3 === 2) & +(5 - 3.5 === 1.5) & +(5.5 - 3.5 === 2.0) & +(1 - 0 === 1) & +(2.5 - 0 === 2.5) & +(2 * 3 === 6) & +(2 * 3.5 === 7.0) & +(2.5 * 3.5 === 8.75) & +(2.5 * 0 === 0.0) & +(10 / 2 === 5) & +(10 / 2.5 === 4.0) & +(18 / 36 === 0.5) & +(0 / 36 === 0) & +(12.5 / 2.5 === 5.0) & +(10.5 / 2.5 === 4.2) & +(10 % 7 === 3) & +(10.48762 % 7 === 3) & +(10 % 7.123576 === 3) & +(2 ** 4 === 16) & +(2.5 ** 2 === 6.25) & +(2.5 ** 0 === 1.0) & +(1000 ** 0 === 1)
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/list-assignment.r b/AbuseFilter/tests/parserTests/array-assignment.r index 4736e080..4736e080 100644 --- a/AbuseFilter/tests/parserTests/list-assignment.r +++ b/AbuseFilter/tests/parserTests/array-assignment.r diff --git a/AbuseFilter/tests/parserTests/array-assignment.t b/AbuseFilter/tests/parserTests/array-assignment.t new file mode 100644 index 00000000..a22294e3 --- /dev/null +++ b/AbuseFilter/tests/parserTests/array-assignment.t @@ -0,0 +1,6 @@ +test_array := [ [1, 2], [3, 4] ]; + +test_array[1] := 42; +test_array[] := 17; + +test_array[0][0] == 1 & test_array[0][1] == 2 & test_array[1] == 42 & test_array[2] == 17 diff --git a/AbuseFilter/tests/parserTests/list-inequality.r b/AbuseFilter/tests/parserTests/array-comparisons.r index 4736e080..4736e080 100644 --- a/AbuseFilter/tests/parserTests/list-inequality.r +++ b/AbuseFilter/tests/parserTests/array-comparisons.r diff --git a/AbuseFilter/tests/parserTests/array-comparisons.t b/AbuseFilter/tests/parserTests/array-comparisons.t new file mode 100644 index 00000000..dc6122f5 --- /dev/null +++ b/AbuseFilter/tests/parserTests/array-comparisons.t @@ -0,0 +1,15 @@ +a := [1, 2, 3]; +b := [1, 2, 3]; +c := [2, 3, 4]; +d := [1, 2, 3, 4]; +e := ['1', '2', '3']; +f := [[['1']]]; +g := [[[1]]]; +h := [[1, 2], 3]; +i := [['1', 2], '3']; +j := [1]; +k := ['1']; +l := []; + +a == b & a === b & a != c & b != d & a == e & a !== e & f == g & f !== g & h == i & h !== i & e != i & j != 1 & +k != '1' & l == false & l == null & l !== false & l !== null & false == l & null == l & false !== l & null !== l diff --git a/AbuseFilter/tests/parserTests/atombraces.r b/AbuseFilter/tests/parserTests/atombraces.r new file mode 100644 index 00000000..f4efce9f --- /dev/null +++ b/AbuseFilter/tests/parserTests/atombraces.r @@ -0,0 +1 @@ +NOT MATCH diff --git a/AbuseFilter/tests/parserTests/atombraces.t b/AbuseFilter/tests/parserTests/atombraces.t new file mode 100644 index 00000000..5bb50190 --- /dev/null +++ b/AbuseFilter/tests/parserTests/atombraces.t @@ -0,0 +1 @@ +( ) diff --git a/AbuseFilter/tests/parserTests/cast.t b/AbuseFilter/tests/parserTests/cast.t index 0ec2720e..c0bc317c 100644 --- a/AbuseFilter/tests/parserTests/cast.t +++ b/AbuseFilter/tests/parserTests/cast.t @@ -1 +1 @@ -(string(1) === "1") & (int("1") === 1) & (float(1) === 1.0) +(string(1) === "1") & (int("1") === 1) & (float(1) === 1.0) & bool(1) & !bool(0) diff --git a/AbuseFilter/tests/parserTests/shortcircuit.r b/AbuseFilter/tests/parserTests/ccnorm-contains-all.r index 4736e080..4736e080 100644 --- a/AbuseFilter/tests/parserTests/shortcircuit.r +++ b/AbuseFilter/tests/parserTests/ccnorm-contains-all.r diff --git a/AbuseFilter/tests/parserTests/ccnorm-contains-all.t b/AbuseFilter/tests/parserTests/ccnorm-contains-all.t new file mode 100644 index 00000000..9a8635c8 --- /dev/null +++ b/AbuseFilter/tests/parserTests/ccnorm-contains-all.t @@ -0,0 +1 @@ +ccnorm_contains_all("the f00 is on the b4r", "foo", "is on", "bar") diff --git a/AbuseFilter/tests/parserTests/whitespace.r b/AbuseFilter/tests/parserTests/ccnorm-contains-any.r index 4736e080..4736e080 100644 --- a/AbuseFilter/tests/parserTests/whitespace.r +++ b/AbuseFilter/tests/parserTests/ccnorm-contains-any.r diff --git a/AbuseFilter/tests/parserTests/ccnorm-contains-any.t b/AbuseFilter/tests/parserTests/ccnorm-contains-any.t new file mode 100644 index 00000000..6aeac35c --- /dev/null +++ b/AbuseFilter/tests/parserTests/ccnorm-contains-any.t @@ -0,0 +1 @@ +ccnorm_contains_any("like 4ny0ne else", "foo", "aNyon3") & ccnorm_contains_any("street f1ghter","F1ght") diff --git a/AbuseFilter/tests/parserTests/comment.t b/AbuseFilter/tests/parserTests/comment.t index 2ddf5829..3252b87a 100644 --- a/AbuseFilter/tests/parserTests/comment.t +++ b/AbuseFilter/tests/parserTests/comment.t @@ -1 +1,4 @@ -1 /* a */ == /* b */ "1" /* c */ +1 /* a */ == /* b */ "1" /* c */ /* & + +1/0 +*/ diff --git a/AbuseFilter/tests/parserTests/concatenation.r b/AbuseFilter/tests/parserTests/concatenation.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/concatenation.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/concatenation.t b/AbuseFilter/tests/parserTests/concatenation.t new file mode 100644 index 00000000..c4a8065f --- /dev/null +++ b/AbuseFilter/tests/parserTests/concatenation.t @@ -0,0 +1,7 @@ +'foo' + 'bar' === 'foobar' & +'' + 'foo' + '' === 'foo' & +'foo' + ' ' + 'bar' === 'foo bar' & +'foo' + 234 === 'foo234' & +452 + 'foo' === '452foo' & +'foo' + false === 'foo' & +'foo' + [ 'bar', 'foo' ] === 'foobar\nfoo\n' diff --git a/AbuseFilter/tests/parserTests/contains-all.r b/AbuseFilter/tests/parserTests/contains-all.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/contains-all.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/contains-all.t b/AbuseFilter/tests/parserTests/contains-all.t new file mode 100644 index 00000000..f8b81b24 --- /dev/null +++ b/AbuseFilter/tests/parserTests/contains-all.t @@ -0,0 +1 @@ +contains_all("the foo is on the bar", "foo", "is on", "bar") & !(contains_all(['foo', 'bar', 'hey'], 'foo', 'bar', 'sup')) & contains_all([1, 2, 3], '1', '2', '3')
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/contains-any.r b/AbuseFilter/tests/parserTests/contains-any.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/contains-any.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/contains-any.t b/AbuseFilter/tests/parserTests/contains-any.t new file mode 100644 index 00000000..56f1c8bd --- /dev/null +++ b/AbuseFilter/tests/parserTests/contains-any.t @@ -0,0 +1 @@ +contains_any("like anyone else", "else", "someone") & contains_any("street fighter", "fight") & !(contains_any('My foo is cute', 'bar', 'wtf')) & contains_any([[1], [2,3]], 1) diff --git a/AbuseFilter/tests/parserTests/contains.r b/AbuseFilter/tests/parserTests/contains.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/contains.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/contains.t b/AbuseFilter/tests/parserTests/contains.t new file mode 100644 index 00000000..5aeb638a --- /dev/null +++ b/AbuseFilter/tests/parserTests/contains.t @@ -0,0 +1,7 @@ +"quux" contains "ux" & +['1', 'foo'] contains '1' & +'fo obar' contains 'foo' === false & +['foo'] contains 'f' & +'' contains 'a' === false & +'a' contains '' === false & +'' contains '' === false
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/containsfunction.r b/AbuseFilter/tests/parserTests/containsfunction.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/containsfunction.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/containsfunction.t b/AbuseFilter/tests/parserTests/containsfunction.t new file mode 100644 index 00000000..418c9e2c --- /dev/null +++ b/AbuseFilter/tests/parserTests/containsfunction.t @@ -0,0 +1,2 @@ +contains_any( "", "a") === false & +contains_any( "a", "", "a") === true
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/count.t b/AbuseFilter/tests/parserTests/count.t index 5733b0b2..cc9e719f 100644 --- a/AbuseFilter/tests/parserTests/count.t +++ b/AbuseFilter/tests/parserTests/count.t @@ -1,6 +1,8 @@ -count("a,b,c,d") = 4 & -count(",", "a,b,c,d") = 3 & -count("", "abcd") = 0 & -count("a", "abab") = 2 & -count("ab", "abab") = 2 & -count("aa", "aaaaa") = 2 +count("a,b,c,d") === 4 & +count(",", "a,b,c,d") === 3 & +count("", "abcd") === 0 & +count("a", "abab") === 2 & +count("ab", "abab") === 2 & +count("aa", "aaaaa") === 2 & +count( [ "a", "b", "c" ] ) === 3 & +count( [] ) === 0 diff --git a/AbuseFilter/tests/parserTests/equals-to-any.r b/AbuseFilter/tests/parserTests/equals-to-any.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/equals-to-any.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/equals-to-any.t b/AbuseFilter/tests/parserTests/equals-to-any.t new file mode 100644 index 00000000..e10220ca --- /dev/null +++ b/AbuseFilter/tests/parserTests/equals-to-any.t @@ -0,0 +1,4 @@ +equals_to_any( "foo", "bar", "foo", "pizza" ) & +equals_to_any( 15, 3, 77, 18, 15 ) & +equals_to_any( "", 3, 77, 18, 15, "duh" ) === false & +equals_to_any( "", 3, 77, 18, 15, "duh", "" )
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/expn.t b/AbuseFilter/tests/parserTests/expn.t index bd39c386..17849182 100644 --- a/AbuseFilter/tests/parserTests/expn.t +++ b/AbuseFilter/tests/parserTests/expn.t @@ -1,2 +1,2 @@ /* In filter language, the exponentiation is left-associative */ -(2 ** 3 ** 2) == 64 +(2 ** 3 ** 2) === 64 diff --git a/AbuseFilter/tests/parserTests/float.t b/AbuseFilter/tests/parserTests/float.t index 9ce8c919..e31067e0 100644 --- a/AbuseFilter/tests/parserTests/float.t +++ b/AbuseFilter/tests/parserTests/float.t @@ -1 +1 @@ -(5 / 2 = 2) & (5. / 2 = 2.5) & (5 / 2. = 2.5) & (int(.5) = 0) +(5 / 2 === 2.5) & (int(5 / 2) === 2) & (5. / 2 === 2.5) & (5 / 2. === 2.5) & (int(.5) === 0) diff --git a/AbuseFilter/tests/parserTests/get-matches.r b/AbuseFilter/tests/parserTests/get-matches.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/get-matches.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/get-matches.t b/AbuseFilter/tests/parserTests/get-matches.t new file mode 100644 index 00000000..88e163b4 --- /dev/null +++ b/AbuseFilter/tests/parserTests/get-matches.t @@ -0,0 +1,4 @@ +/* More complete tests for get_matches are in AbuseFilterParserTest.php */ +a := get_matches('I am a (dog|cat)', 'What did you say?'); +get_matches('The (truth|pineapple) is (?:rarely)? pure and (nee*v(ah|er) sh?imple)', 'The truth is rarely pure and never simple, Wilde said') == ['The truth is rarely pure and never simple', 'truth', 'never simple', 'er'] & +a === [false, false]
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/ifthen.t b/AbuseFilter/tests/parserTests/ifthen.t index 160d1ab8..c22a3947 100644 --- a/AbuseFilter/tests/parserTests/ifthen.t +++ b/AbuseFilter/tests/parserTests/ifthen.t @@ -1 +1,2 @@ -(if 1 then 2 else 3 end) == 2 +(if 1 then 2 else 3 end) === 2 & +(if false then 2 else 3 end) === 3 diff --git a/AbuseFilter/tests/parserTests/in.t b/AbuseFilter/tests/parserTests/in.t index 467639ce..73d480d3 100644 --- a/AbuseFilter/tests/parserTests/in.t +++ b/AbuseFilter/tests/parserTests/in.t @@ -1 +1 @@ -"foo" in "foobar" & "quux" contains "ux" +"foo" in "foobar" & '1' in ['1', 'foo'] & !('foo' in 'fo obar') & 'f' in ['foo'] diff --git a/AbuseFilter/tests/parserTests/lazyboolinvert.r b/AbuseFilter/tests/parserTests/lazyboolinvert.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazyboolinvert.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/lazyboolinvert.t b/AbuseFilter/tests/parserTests/lazyboolinvert.t new file mode 100644 index 00000000..71f5f213 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazyboolinvert.t @@ -0,0 +1 @@ +1 === 1 | !true diff --git a/AbuseFilter/tests/parserTests/lazyfunction.r b/AbuseFilter/tests/parserTests/lazyfunction.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazyfunction.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/lazyfunction.t b/AbuseFilter/tests/parserTests/lazyfunction.t new file mode 100644 index 00000000..4c77b374 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazyfunction.t @@ -0,0 +1 @@ +1 === 1 | contains_any( "a", "b") diff --git a/AbuseFilter/tests/parserTests/lazykeyword.r b/AbuseFilter/tests/parserTests/lazykeyword.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazykeyword.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/lazykeyword.t b/AbuseFilter/tests/parserTests/lazykeyword.t new file mode 100644 index 00000000..da680aef --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazykeyword.t @@ -0,0 +1 @@ +1 === 1 | "a" like "b" diff --git a/AbuseFilter/tests/parserTests/lazypow.r b/AbuseFilter/tests/parserTests/lazypow.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazypow.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/lazypow.t b/AbuseFilter/tests/parserTests/lazypow.t new file mode 100644 index 00000000..8b650e27 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazypow.t @@ -0,0 +1 @@ +1 === 1 | 2**3 === 8 diff --git a/AbuseFilter/tests/parserTests/lazysum.r b/AbuseFilter/tests/parserTests/lazysum.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazysum.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/lazysum.t b/AbuseFilter/tests/parserTests/lazysum.t new file mode 100644 index 00000000..9fedb3f3 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazysum.t @@ -0,0 +1 @@ +1 === 1 | 2 + 3 === 5 diff --git a/AbuseFilter/tests/parserTests/lazyunarys.r b/AbuseFilter/tests/parserTests/lazyunarys.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazyunarys.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/lazyunarys.t b/AbuseFilter/tests/parserTests/lazyunarys.t new file mode 100644 index 00000000..51a1ce3e --- /dev/null +++ b/AbuseFilter/tests/parserTests/lazyunarys.t @@ -0,0 +1 @@ +1 === 1 | -4 !== 4 diff --git a/AbuseFilter/tests/parserTests/list-assignment.t b/AbuseFilter/tests/parserTests/list-assignment.t deleted file mode 100644 index c055541b..00000000 --- a/AbuseFilter/tests/parserTests/list-assignment.t +++ /dev/null @@ -1,6 +0,0 @@ -test_list := [ [1, 2], [3, 4] ]; - -test_list[1] := 42; -test_list[] := 17; - -test_list[0][0] == 1 & test_list[0][1] == 2 & test_list[1] == 42 & test_list[2] == 17 diff --git a/AbuseFilter/tests/parserTests/list-inequality.t b/AbuseFilter/tests/parserTests/list-inequality.t deleted file mode 100644 index 6ffed4f0..00000000 --- a/AbuseFilter/tests/parserTests/list-inequality.t +++ /dev/null @@ -1,3 +0,0 @@ -a := [1, 2, 3]; - -a != a diff --git a/AbuseFilter/tests/parserTests/multipleskipbraces.r b/AbuseFilter/tests/parserTests/multipleskipbraces.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/multipleskipbraces.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/multipleskipbraces.t b/AbuseFilter/tests/parserTests/multipleskipbraces.t new file mode 100644 index 00000000..ea8def7f --- /dev/null +++ b/AbuseFilter/tests/parserTests/multipleskipbraces.t @@ -0,0 +1 @@ +1 === 1 | ( ( ( 3 === 2 ) ) ) diff --git a/AbuseFilter/tests/parserTests/mwexamples-arithmetic.r b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-arithmetic.t b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.t new file mode 100644 index 00000000..7cfaf4af --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.t @@ -0,0 +1,8 @@ +/* Examples from [[mw:Extension:AbuseFilter/Rules format#Arithmetic]] */ + +1 + 1 === 2 & +2 * 2 === 4 & +12 / 24 === 0.5 & +24 / 12 === 2 & +9 ** 2 === 81 & +6 % 5 === 1
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-arrays.r b/AbuseFilter/tests/parserTests/mwexamples-arrays.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-arrays.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-arrays.t b/AbuseFilter/tests/parserTests/mwexamples-arrays.t new file mode 100644 index 00000000..91538fed --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-arrays.t @@ -0,0 +1,10 @@ +/* Examples from [[mw:Extension:AbuseFilter/Rules format#Lists]] */ + +a_array := [ 5, 6, 7, 10]; +a_array[0] == 5 & +length(a_array) == 4 & +string(a_array) == "5\n6\n7\n10\n" & +5 in a_array == true & +'5' in a_array == true & +'5\n6' in a_array == true & +1 in a_array == true
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-bools.r b/AbuseFilter/tests/parserTests/mwexamples-bools.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-bools.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-bools.t b/AbuseFilter/tests/parserTests/mwexamples-bools.t new file mode 100644 index 00000000..a8e88539 --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-bools.t @@ -0,0 +1,13 @@ +/* Examples from [[mw:Extension:AbuseFilter/Rules format#Boolean operations]] */ + +(1 | 1) & +(1 | 0) & +!(0 | 0) & +(1 & 1) & +!(1 & 0) & +!(0 & 0) & +!(1 ^ 1) & +(1 ^ 0) & +!(0 ^ 0) & +!(!1) + diff --git a/AbuseFilter/tests/parserTests/mwexamples-comparisons.r b/AbuseFilter/tests/parserTests/mwexamples-comparisons.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-comparisons.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-comparisons.t b/AbuseFilter/tests/parserTests/mwexamples-comparisons.t new file mode 100644 index 00000000..76582590 --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-comparisons.t @@ -0,0 +1,22 @@ +/* Examples from [[mw:Extension:AbuseFilter/Rules format#Simple comparisons]] */ + +!(1 == 2) & +(1 <= 2) & +!(1 >= 2) & +(1 != 2) & +(1 < 2) & +!(1 > 2) & +(2 = 2) & +('' == false) & +!('' === false) & +(1 == true) & +!(1 === true) & +(['1','2','3'] == ['1','2','3']) & +([1,2,3] === [1,2,3]) & +(['1','2','3'] == [1,2,3]) & +!(['1','2','3'] === [1,2,3]) & +([1,1,''] == [true, true, false]) & +([] == false) & +([] == null) & +!(['1'] == '1') + diff --git a/AbuseFilter/tests/parserTests/mwexamples-functions.r b/AbuseFilter/tests/parserTests/mwexamples-functions.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-functions.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-functions.t b/AbuseFilter/tests/parserTests/mwexamples-functions.t new file mode 100644 index 00000000..ea5bf1b1 --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-functions.t @@ -0,0 +1,22 @@ +/* Examples from [[mw:Extension:AbuseFilter/Rules format#Functions]] */ + +length( "Wikipedia" ) === 9 & +lcase( "WikiPedia" ) === 'wikipedia' & +ccnorm( "w1k1p3d14" ) === 'WIKIPEDIA' & +ccnorm( "ωɨƙɩᑭƐƉ1α" ) === 'WIKIPEDIA' & +ccnorm_contains_any( "w1k1p3d14", "wiKiP3D1A", "foo", "bar" ) === true & +ccnorm_contains_any( "w1k1p3d14", "foo", "bar", "baz" ) === false & +ccnorm_contains_any( "w1k1p3d14 is 4w3s0me", "bar", "baz", "some" ) === true & +ccnorm( "ìíîïĩїį!ľ₤ĺľḷĿ" ) === 'IIIIIII!LLLLLL' & +norm( "!!ω..ɨ..ƙ..ɩ..ᑭᑭ..Ɛ.Ɖ@@1%%α!!" ) === 'WIKIPEDAIA' & +norm( "F00 B@rr" ) === 'FOBAR' & +rmdoubles( "foobybboo" ) === 'fobybo' & +specialratio( "Wikipedia!" ) === 0.1 & +count( "foo", "foofooboofoo" ) === 3 & +count( "foo,bar,baz" ) === 3 & +rmspecials( "FOOBAR!!1" ) === 'FOOBAR1' & +rescape( "abc* (def)" ) === 'abc\* \(def\)' & +str_replace( "foobarbaz", "bar", "-" ) === 'foo-baz' & +ip_in_range( "127.0.10.0", "127.0.0.0/12" ) === true & +contains_any( "foobar", "x", "y", "f" ) === true & +get_matches( "(foo?ba+r) is (so+ good)", "fobaaar is soooo good to eat" ) === ['fobaaar is soooo good', 'fobaaar', 'soooo good']
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-keywords.r b/AbuseFilter/tests/parserTests/mwexamples-keywords.r new file mode 100644 index 00000000..f629599c --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-keywords.r @@ -0,0 +1 @@ +MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/mwexamples-keywords.t b/AbuseFilter/tests/parserTests/mwexamples-keywords.t new file mode 100644 index 00000000..d2fbb4a5 --- /dev/null +++ b/AbuseFilter/tests/parserTests/mwexamples-keywords.t @@ -0,0 +1,10 @@ +/* Examples from [[mw:Extension:AbuseFilter/Rules format#Keywords]] */ + +("1234" like "12?4") & +("1234" like "12*") & +("foo" in "foobar") & +("foobar" contains "foo") & +("o" in ["foo", "bar"]) & +("foo" regex "\w+") & +("a\b" regex "a\\\\b") & +("a\b" regex "a\x5C\x5Cb")
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/ord.t b/AbuseFilter/tests/parserTests/ord.t index a82aafa7..6c94f4b6 100644 --- a/AbuseFilter/tests/parserTests/ord.t +++ b/AbuseFilter/tests/parserTests/ord.t @@ -1 +1,8 @@ -(1 > 0) & (0 < 1) & (2 >= 2) & (2 <= 2) +1 > 0 & +0 < 1 & +2 >= 2 & +2 <= 2 & +0.1 < 0.2 & +0.001 <= 0.001 & +-0.01 < 0.01 & +0 >= -0.0001 diff --git a/AbuseFilter/tests/parserTests/rcount.r b/AbuseFilter/tests/parserTests/rcount.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/rcount.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/rcount.t b/AbuseFilter/tests/parserTests/rcount.t new file mode 100644 index 00000000..a514dad5 --- /dev/null +++ b/AbuseFilter/tests/parserTests/rcount.t @@ -0,0 +1,3 @@ +rcount("a,b,c,d") = 4 & +rcount(".", "abcd") = 4 + diff --git a/AbuseFilter/tests/parserTests/rmwhitespace.r b/AbuseFilter/tests/parserTests/rmwhitespace.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/rmwhitespace.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/rmwhitespace.t b/AbuseFilter/tests/parserTests/rmwhitespace.t new file mode 100644 index 00000000..d6da2114 --- /dev/null +++ b/AbuseFilter/tests/parserTests/rmwhitespace.t @@ -0,0 +1,2 @@ +rmwhitespace( "foobar" ) === "foobar" & +rmwhitespace( "foo bar bar foo" ) === "foobarbarfoo" diff --git a/AbuseFilter/tests/parserTests/sanitize.r b/AbuseFilter/tests/parserTests/sanitize.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/sanitize.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/sanitize.t b/AbuseFilter/tests/parserTests/sanitize.t new file mode 100644 index 00000000..06036c0c --- /dev/null +++ b/AbuseFilter/tests/parserTests/sanitize.t @@ -0,0 +1 @@ +sanitize('یک') = 'یک' diff --git a/AbuseFilter/tests/parserTests/shortcircuit-and.r b/AbuseFilter/tests/parserTests/shortcircuit-and.r new file mode 100644 index 00000000..33a8a805 --- /dev/null +++ b/AbuseFilter/tests/parserTests/shortcircuit-and.r @@ -0,0 +1 @@ +NOT_MATCH
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/shortcircuit-and.t b/AbuseFilter/tests/parserTests/shortcircuit-and.t new file mode 100644 index 00000000..d87746de --- /dev/null +++ b/AbuseFilter/tests/parserTests/shortcircuit-and.t @@ -0,0 +1,2 @@ +/* The division by zero should not be executed and not crash the filter */ +false & 1/0
\ No newline at end of file diff --git a/AbuseFilter/tests/parserTests/shortcircuit-or.r b/AbuseFilter/tests/parserTests/shortcircuit-or.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/shortcircuit-or.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/shortcircuit.t b/AbuseFilter/tests/parserTests/shortcircuit-or.t index bec088f6..bec088f6 100644 --- a/AbuseFilter/tests/parserTests/shortcircuit.t +++ b/AbuseFilter/tests/parserTests/shortcircuit-or.t diff --git a/AbuseFilter/tests/parserTests/specialratio.t b/AbuseFilter/tests/parserTests/specialratio.t index c4a3565a..5a1380a8 100644 --- a/AbuseFilter/tests/parserTests/specialratio.t +++ b/AbuseFilter/tests/parserTests/specialratio.t @@ -1 +1,2 @@ -specialratio("foó;") = 0.25 +specialratio("foó;") === 0.25 & +specialratio("") === 0.0 diff --git a/AbuseFilter/tests/parserTests/string.t b/AbuseFilter/tests/parserTests/string.t index f6036cef..47685ed9 100644 --- a/AbuseFilter/tests/parserTests/string.t +++ b/AbuseFilter/tests/parserTests/string.t @@ -1 +1,6 @@ -"a\tb" = "a b" & "a\qb" = "a\qb" +"a\tb" === "a b" & +"a\qb" === "a\qb" & +"a\"b" === 'a"b' & +"a\rb" !== "a\r\nb" & +"\x66\x6f\x6f" === "foo" & +"some\xstring" === "somexstring" diff --git a/AbuseFilter/tests/parserTests/strpos.r b/AbuseFilter/tests/parserTests/strpos.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/strpos.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/strpos.t b/AbuseFilter/tests/parserTests/strpos.t new file mode 100644 index 00000000..65312318 --- /dev/null +++ b/AbuseFilter/tests/parserTests/strpos.t @@ -0,0 +1,4 @@ +strpos( "foobarfoo", "foo" ) === 0 & +strpos( "foobarfoo", "" ) === -1 & +strpos( "foobarfoo", "foo", 1 ) === 6 & +strpos( "foobarfoo", "lol" ) === -1 diff --git a/AbuseFilter/tests/parserTests/substr.r b/AbuseFilter/tests/parserTests/substr.r new file mode 100644 index 00000000..4736e080 --- /dev/null +++ b/AbuseFilter/tests/parserTests/substr.r @@ -0,0 +1 @@ +MATCH diff --git a/AbuseFilter/tests/parserTests/substr.t b/AbuseFilter/tests/parserTests/substr.t new file mode 100644 index 00000000..b1135066 --- /dev/null +++ b/AbuseFilter/tests/parserTests/substr.t @@ -0,0 +1,2 @@ +substr( "foobar", 0, 3 ) === "foo" & +substr( "barfoo", 4 ) === "oo" diff --git a/AbuseFilter/tests/phan/config.php b/AbuseFilter/tests/phan/config.php new file mode 100644 index 00000000..99685d08 --- /dev/null +++ b/AbuseFilter/tests/phan/config.php @@ -0,0 +1,19 @@ +<?php + +$cfg = require __DIR__ . '/../../vendor/mediawiki/mediawiki-phan-config/src/config.php'; + +$cfg['directory_list'] = array_merge( + $cfg['directory_list'], + [ + './../../extensions/CheckUser', + ] +); + +$cfg['exclude_analysis_directory_list'] = array_merge( + $cfg['exclude_analysis_directory_list'], + [ + './../../extensions/CheckUser', + ] +); + +return $cfg; diff --git a/AbuseFilter/tests/phpunit/AFPDataTest.php b/AbuseFilter/tests/phpunit/AFPDataTest.php new file mode 100644 index 00000000..88315da4 --- /dev/null +++ b/AbuseFilter/tests/phpunit/AFPDataTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Tests for the AFPData class + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GPL-2.0-or-later + */ + +/** + * @group Test + * @group AbuseFilter + * + * @covers AFPData + * @covers AbuseFilterTokenizer + * @covers AFPToken + * @covers AFPUserVisibleException + * @covers AFPException + * @covers AbuseFilterParser + */ +class AFPDataTest extends MediaWikiTestCase { + /** + * @return AbuseFilterParser + */ + public static function getParser() { + static $parser = null; + if ( !$parser ) { + $parser = new AbuseFilterParser(); + } else { + $parser->resetState(); + } + return $parser; + } + + /** + * Base method for testing exceptions + * + * @param string $excep Identifier of the exception (e.g. 'unexpectedtoken') + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + */ + private function exceptionTest( $excep, $expr, $caller ) { + $parser = self::getParser(); + try { + $parser->parse( $expr ); + } catch ( AFPUserVisibleException $e ) { + $this->assertEquals( + $excep, + $e->mExceptionID, + "Exception $excep not thrown in AFPData::$caller" + ); + return; + } + + $this->fail( "Exception $excep not thrown in AFPData::$caller" ); + } + + /** + * Test the 'regexfailure' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AFPData::keywordRegex + * @dataProvider regexFailure + */ + public function testRegexFailureException( $expr, $caller ) { + $this->exceptionTest( 'regexfailure', $expr, $caller ); + } + + /** + * Data provider for testRegexFailureException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function regexFailure() { + return [ + [ "'a' rlike '('", 'keywordRegex' ], + ]; + } + + /** + * Test the 'dividebyzero' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AFPData::mulRel + * @dataProvider divideByZero + */ + public function testDivideByZeroException( $expr, $caller ) { + $this->exceptionTest( 'dividebyzero', $expr, $caller ); + } + + /** + * Data provider for testRegexFailureException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function divideByZero() { + return [ + [ '1/0', 'mulRel' ], + ]; + } +} diff --git a/AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php b/AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php new file mode 100644 index 00000000..4be865b5 --- /dev/null +++ b/AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php @@ -0,0 +1,902 @@ +<?php +/** + * Complete tests where filters are saved, actions are executed and the right + * consequences are expected to be taken + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GPL-2.0-or-later + */ + +/** + * @group Test + * @group AbuseFilter + * @group AbuseFilterConsequences + * @group Database + * + * @covers AbuseFilter + * @covers AbuseFilterHooks + * @covers AbuseFilterParser + * @covers AFPData + * @covers AbuseFilterTokenizer + * @covers AFPToken + * @covers AbuseFilterVariableHolder + * @covers AFComputedVariable + */ +class AbuseFilterConsequencesTest extends MediaWikiTestCase { + protected static $mUser; + + /** + * @var array This tables will be deleted in parent::tearDown + */ + protected $tablesUsed = [ + 'abuse_filter', + 'abuse_filter_action', + 'abuse_filter_history', + 'abuse_filter_log', + 'page' + ]; + + // Properties of the filter rows that we're not interested in changing. + // Write them once to save space + protected static $defaultRowSection = [ + 'af_user_text' => 'FilterTester', + 'af_user' => 0, + 'af_timestamp' => '20180707105743', + 'af_group' => 'default', + 'af_hit_count' => 0, + ]; + + // Filters that may be created, their key is the ID. + protected static $filters = [ + 1 => [ + 'af_id' => 1, + 'af_pattern' => 'added_lines irlike "foo"', + 'af_enabled' => 1, + 'af_comments' => 'Comments', + 'af_public_comments' => 'Mock filter for edit', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'warn,tag', + 'af_global' => 0, + 'actions' => [ + 'warn' => [ + 'abusefilter-my-warning' + ], + 'tag' => [ + 'filtertag' + ] + ] + ], + 2 => [ + 'af_id' => 2, + 'af_pattern' => 'action = "move" & moved_to_title contains "test" & moved_to_title === moved_to_text', + 'af_enabled' => 1, + 'af_comments' => 'No comment', + 'af_public_comments' => 'Mock filter for move', + 'af_hidden' => 1, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'disallow,block', + 'af_global' => 0, + 'actions' => [ + 'disallow' => [], + 'block' => [ + 'blocktalk', + '8 hours', + 'infinity' + ] + ] + ], + 3 => [ + 'af_id' => 3, + 'af_pattern' => 'action = "delete" & "test" in lcase(page_prefixedtitle) & page_prefixedtitle === article_prefixedtext', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter for delete', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'degroup', + 'af_global' => 0, + 'actions' => [ + 'degroup' => [] + ] + ], + 4 => [ + 'af_id' => 4, + 'af_pattern' => 'action contains "createaccount" & accountname rlike "user" & page_title === article_text', + 'af_enabled' => 1, + 'af_comments' => '1', + 'af_public_comments' => 'Mock filter for createaccount', + 'af_hidden' => 1, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => '', + 'af_global' => 0, + 'actions' => [] + ], + 5 => [ + 'af_id' => 5, + 'af_pattern' => 'user_name == "FilteredUser"', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'tag', + 'af_global' => 0, + 'actions' => [ + 'tag' => [ + 'firstTag', + 'secondTag' + ] + ] + ], + 6 => [ + 'af_id' => 6, + 'af_pattern' => 'edit_delta === 7', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter with edit_delta', + 'af_hidden' => 1, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'disallow', + 'af_global' => 0, + 'actions' => [ + 'disallow' => [] + ] + ], + 7 => [ + 'af_id' => 7, + 'af_pattern' => 'timestamp === int(timestamp)', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter with timestamp', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'degroup', + 'af_global' => 0, + 'actions' => [ + 'degroup' => [] + ] + ], + 8 => [ + 'af_id' => 8, + 'af_pattern' => 'added_lines_pst irlike "\\[\\[Link\\|Link\\]\\]"', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter with pst', + 'af_hidden' => 0, + 'af_hit_count' => 0, + 'af_deleted' => 0, + 'af_actions' => 'disallow,block', + 'af_global' => 0, + 'actions' => [ + 'disallow' => [], + 'block' => [ + 'NoTalkBlockSet', + '4 hours', + '4 hours' + ] + ] + ], + 9 => [ + 'af_id' => 9, + 'af_pattern' => 'new_size > old_size', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter with size', + 'af_hidden' => 1, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'disallow,block', + 'af_global' => 0, + 'actions' => [ + 'disallow' => [], + 'block' => [ + 'blocktalk', + '3 hours', + '3 hours' + ] + ] + ], + 10 => [ + 'af_id' => 10, + 'af_pattern' => '1 == 1', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock throttled filter', + 'af_hidden' => 1, + 'af_throttled' => 1, + 'af_deleted' => 0, + 'af_actions' => 'tag,block', + 'af_global' => 0, + 'actions' => [ + 'tag' => [ + 'testTag' + ], + 'block' => [ + 'blocktalk', + 'infinity', + 'infinity' + ] + ] + ], + 11 => [ + 'af_id' => 11, + 'af_pattern' => '1 == 1', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter which throttles', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'throttle,disallow', + 'af_global' => 0, + 'actions' => [ + 'throttle' => [ + 11, + '1,3600', + 'user' + ], + 'disallow' => [] + ] + ], + 12 => [ + 'af_id' => 12, + 'af_pattern' => 'page_title == user_name & user_name === page_title', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter for userpage', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'disallow,block,degroup', + 'af_global' => 0, + 'actions' => [ + 'disallow' => [], + 'block' => [ + 'blocktalk', + '8 hours', + '1 day' + ], + 'degroup' => [] + ] + ], + 13 => [ + 'af_id' => 13, + 'af_pattern' => '2 == 2', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Another throttled mock filter', + 'af_hidden' => 0, + 'af_throttled' => 1, + 'af_deleted' => 0, + 'af_actions' => 'block,degroup', + 'af_global' => 0, + 'actions' => [ + 'block' => [ + 'blocktalk', + '8 hours', + '1 day' + ], + 'degroup' => [] + ] + ], + 14 => [ + 'af_id' => 14, + 'af_pattern' => '5/int(article_text) == 3', + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Filter with a possible division by zero', + 'af_hidden' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => 'disallow', + 'af_global' => 0, + 'actions' => [ + 'disallow' => [] + ] + ] + ]; + + /** + * @see MediaWikiTestCase::setUp + */ + protected function setUp() { + parent::setUp(); + $user = User::newFromName( 'FilteredUser' ); + $user->addToDatabase(); + $user->addGroup( 'sysop' ); + if ( $user->isBlocked() ) { + $block = Block::newFromTarget( $user ); + $block->delete(); + } + self::$mUser = $user; + // Make sure that the config we're using is the one we're expecting + $this->setMwGlobals( [ + 'wgUser' => $user, + 'wgAbuseFilterActions' => [ + 'throttle' => true, + 'warn' => true, + 'disallow' => true, + 'blockautopromote' => true, + 'block' => true, + 'rangeblock' => true, + 'degroup' => true, + 'tag' => true + ], + 'wgAbuseFilterRuntimeProfile' => true, + 'wgAbuseFilterProfile' => true + ] ); + } + + /** + * Performs an edit. Freely adapted from EditPageTest::assertEdit + * + * @param Title $title Title of the page to edit + * @param string $oldText Old content of the page + * @param string $newText The new content of the page + * @param string $summary The summary of the edit + * @return Status + */ + private static function doEdit( $title, $oldText, $newText, $summary ) { + $page = WikiPage::factory( $title ); + $content = ContentHandler::makeContent( $oldText, $title ); + $page->doEditContent( $content, 'Creating the page for testing AbuseFilter.' ); + + $params = [ + 'wpTextbox1' => $newText, + 'wpSummary' => $summary, + 'wpEditToken' => self::$mUser->getEditToken(), + 'wpEdittime' => $page->getTimestamp(), + 'wpStarttime' => wfTimestampNow(), + 'wpUnicodeCheck' => EditPage::UNICODE_CHECK, + 'wpSectionTitle' => '', + 'wpMinorEdit' => false, + 'wpWatchthis' => false + ]; + $req = new FauxRequest( $params, true ); + + $article = new Article( $title ); + $article->getContext()->setTitle( $title ); + $article->getContext()->setUser( self::$mUser ); + $ep = new EditPage( $article ); + $ep->setContextTitle( $title ); + $ep->importFormData( $req ); + return $ep->internalAttemptSave( $result ); + } + + /** + * Executes an action to filter + * + * @param array $params Parameters of the action + * @param array $options Further options + * @return Status|Status[] + */ + private static function doAction( $params, $options ) { + $type = array_shift( $params ); + $target = array_shift( $params ); + $target = Title::newFromText( $target ); + // Make sure that previous blocks don't affect the test + self::$mUser->clearInstanceCache(); + + switch ( $type ) { + case 'edit': + if ( in_array( 'makeGoodEditFirst', $options ) ) { + $firstStatus = self::doEdit( + $target, $params['oldText'], $params['firstNewText'], $params['summary'] + ); + $secondStatus = self::doEdit( + $target, $params['firstNewText'], $params['secondNewText'], $params['summary'] + ); + $status = [ $firstStatus, $secondStatus ]; + } else { + $status = self::doEdit( $target, $params['oldText'], $params['newText'], $params['summary'] ); + } + break; + case 'move': + $move = new MovePage( $target, Title::newFromText( $params['newTitle'] ) ); + $status = $move->checkPermissions( self::$mUser, 'AbuseFilter move test' ); + break; + case 'delete': + $page = WikiPage::factory( $target ); + $content = ContentHandler::makeContent( 'Page to be deleted in AbuseFilter test', $target ); + $page->doEditContent( $content, 'Creating the page for testing deletion AbuseFilter.' ); + $status = $page->doDeleteArticleReal( 'Testing deletion in AbuseFilter' ); + break; + case 'createaccount': + $user = User::newFromName( $params['username'] ); + $provider = new AbuseFilterPreAuthenticationProvider(); + $status = $provider->testForAccountCreation( $user, $user, [] ); + break; + } + + // Clear cache since we'll need to retrieve some fresh data about the user + // like blocks and groups later when checking expected values + self::$mUser->clearInstanceCache(); + + return $status; + } + + /** + * Creates new filters with the given ids, referred to self::$filters + * + * @param int[] $ids IDs of the filters to create + */ + private static function createFilters( $ids ) { + global $wgAbuseFilterActions; + $dbw = wfGetDB( DB_MASTER ); + + foreach ( $ids as $id ) { + $filter = array_merge( self::$filters[$id], self::$defaultRowSection ); + $actions = $filter['actions']; + unset( $filter['actions'] ); + + $dbw->replace( + 'abuse_filter', + [ 'af_id' ], + $filter, + __METHOD__ + ); + + $actionRows = []; + foreach ( array_filter( $wgAbuseFilterActions ) as $action => $_ ) { + if ( isset( $actions[$action] ) ) { + $parameters = $actions[$action]; + + $thisRow = [ + 'afa_filter' => $id, + 'afa_consequence' => $action, + 'afa_parameters' => implode( "\n", $parameters ) + ]; + $actionsRows[] = $thisRow; + } + } + + $dbw->replace( + 'abuse_filter_action', + [ 'afa_filter' ], + $actionsRows, + __METHOD__ + ); + } + } + + /** + * Creates new filters, execute an action and check the consequences + * + * @param string $testDescription A short description of the test, used for error reporting + * @param int[] $createIds IDs of the filters to create + * @param array $actionParams Details of the action we need to execute to trigger filters + * @param array $consequences The consequences we're expecting + * @param array $options Further options for the test + * @covers AbuseFilter + * @dataProvider provideFilters + */ + public function testFilterConsequences( + $testDescription, + $createIds, + $actionParams, + $consequences, + $options + ) { + global $wgLang; + self::createFilters( $createIds ); + + if ( in_array( 'makeGoodEditFirst', $options ) ) { + $this->setMwGlobals( [ + // Necessary to test throttle + 'wgMainCacheType' => CACHE_ANYTHING + ] ); + } + if ( in_array( 'hitCondsLimit', $options ) ) { + $this->setMwGlobals( [ + 'wgAbuseFilterConditionLimit' => 0 + ] ); + } + if ( in_array( 'hitTimeLimit', $options ) ) { + $this->setMwGlobals( [ + 'wgAbuseFilterSlowFilterRuntimeLimit' => 0 + ] ); + } + if ( in_array( 'hitThrottleLimit', $options ) ) { + $this->setMwGlobals( [ + 'wgAbuseFilterEmergencyDisableCount' => [ + 'default' => 0 + ] + ] ); + } + + $result = self::doAction( $actionParams, $options ); + + $expectedErrors = []; + $testErrorMessage = false; + foreach ( $consequences as $consequence => $ids ) { + foreach ( $ids as $id ) { + $params = self::$filters[$id]['actions'][$consequence]; + $success = true; + switch ( $consequence ) { + case 'warn': + // Aborts the hook with the warning message as error. + $expectedErrors['warn'][] = $params[0]; + break; + case 'disallow': + // Aborts the hook with 'abusefilter-disallowed' error. + $expectedErrors['disallow'][] = 'abusefilter-disallowed'; + break; + case 'block': + // Aborts the hook with 'abusefilter-blocked-display' error. Should block + // the user with expected duration and options. + $userBlock = self::$mUser->getBlock( false ); + + if ( !$userBlock ) { + $testErrorMessage = "User isn't blocked."; + break; + } + + $shouldPreventTalkEdit = $params[0] === 'blocktalk'; + $edittalkCheck = $userBlock->prevents( 'editownusertalk' ) === $shouldPreventTalkEdit; + if ( !$edittalkCheck ) { + $testErrorMessage = 'The expected block option "edittalk" options does not ' . + 'match the actual one.'; + break; + } + + $expectedExpiry = SpecialBlock::parseExpiryInput( $params[2] ); + // Get rid of non-numeric 'infinity' by setting it to 0 + $actualExpiry = wfIsInfinity( $userBlock->getExpiry() ) ? 0 : $userBlock->getExpiry(); + $expectedExpiry = wfIsInfinity( $expectedExpiry ) ? 0 : $expectedExpiry; + // We need to take into account code execution time. 10 seconds should be enough + $durationCheck = abs( strtotime( $actualExpiry ) - strtotime( $expectedExpiry ) ) < 10; + if ( !$durationCheck ) { + $testErrorMessage = "The expected block expiry ($expectedExpiry) does not " . + "match the actual one ($actualExpiry)."; + break; + } + + $expectedErrors['block'][] = 'abusefilter-blocked-display'; + break; + case 'degroup': + // Aborts the hook with 'abusefilter-degrouped' error and degroups the user. + $expectedErrors['degroup'][] = 'abusefilter-degrouped'; + $groupCheck = !in_array( 'sysop', self::$mUser->getEffectiveGroups() ); + if ( !$groupCheck ) { + $testErrorMessage = 'The user was not degrouped.'; + } + break; + case 'tag': + // Only add tags, to be retrieved in tag_summary table. + if ( $actionParams[1] === null ) { + // It's an account creation, so no tags. + break; + } + $title = Title::newFromText( $actionParams[1] ); + $page = WikiPage::factory( $title ); + $revId = $page->getLatest(); + $dbr = wfGetDB( DB_REPLICA ); + $appliedTags = $dbr->selectField( + 'tag_summary', + 'ts_tags', + [ 'ts_rev_id' => $revId ], + __METHOD__ + ); + $appliedTags = explode( ',', $appliedTags ); + + $tagCheck = count( array_diff( $params, $appliedTags ) ) === 0; + if ( !$tagCheck ) { + $expectedTags = $wgLang->commaList( $params ); + $actualTags = $wgLang->commaList( $appliedTags ); + + $testErrorMessage = "Expected the edit to have the following tags: $expectedTags. " . + "Got the following instead: $actualTags."; + } + break; + case 'throttle': + // The action was executed twice and $result is an array of two Status objects. + if ( !$result[0]->isGood() ) { + // The first one should be fine + $testErrorMessage = "The first edit should have been saved, being only throttled."; + break; + } + + $result = $result[1]; + break; + } + + if ( $testErrorMessage ) { + $this->fail( "$testErrorMessage Test description: $testDescription" ); + } + } + } + + if ( in_array( 'hitThrottleLimit', $options ) ) { + $dbr = wfGetDB( DB_REPLICA ); + $throttled = true; + foreach ( $createIds as $filter ) { + $curThrottle = $dbr->selectField( + 'abuse_filter', + 'af_throttled', + [ 'af_id' => $filter ], + __METHOD__ + ); + $throttled &= $curThrottle; + } + + if ( !$throttled ) { + $expectedThrottled = $wgLang->commaList( $createIds ); + $this->fail( 'Expected the following filters to be automatically ' . + "throttled: $expectedThrottled." ); + } + } + + // Errors have a priority order + $expected = $expectedErrors['warn'] ?? $expectedErrors['degroup'] ?? + $expectedErrors['block'] ?? $expectedErrors['disallow'] ?? null; + if ( isset( $expectedErrors['degroup'] ) && $expected === $expectedErrors['degroup'] && + isset( $expectedErrors['block'] ) ) { + // Degroup and block warning can be fired together + $expected = array_merge( $expectedErrors['degroup'], $expectedErrors['block'] ); + } elseif ( !is_array( $expected ) ) { + $expected = (array)$expected; + } + + $errors = $result->getErrors(); + + $actual = []; + foreach ( $errors as $error ) { + $msg = $error['message']; + if ( strpos( $msg, 'abusefilter' ) !== false ) { + $actual[] = $msg; + } + } + + $expectedDisplay = $wgLang->commaList( $expected ); + $actualDisplay = $wgLang->commaList( $actual ); + + $this->assertEquals( + $expected, + $actual, + "The edit should have returned the following error messages: $expectedDisplay. " . + "Got $actualDisplay instead. Test description: $testDescription" + ); + } + + /** + * Data provider for creating and editing filters. For every test case, we pass + * - an array with the IDs of the filters to be created (listed in self::$filters), + * - an array with details of the action to execute in order to trigger the filters, + * - an array of expected consequences of the form + * [ 'consequence name' => [ IDs of the filter to take its parameters from ] ] + * Such IDs may be more than one if we have a warning that is shown twice. + * - an array with further options for testing + * + * @return array + */ + public function provideFilters() { + return [ + [ + 'Basic test for "edit" action.', + [ 1, 2 ], + [ + 'edit', + 'Test page', + 'oldText' => 'Some old text for the test.', + 'newText' => 'I like foo', + 'summary' => 'Test AbuseFilter for edit action.' + ], + [ 'warn' => [ 1 ] ], + [] + ], + [ + 'Basic test for "move" action.', + [ 2 ], + [ + 'move', + 'Test page', + 'newTitle' => 'Another test page' + ], + [ 'disallow' => [ 2 ], 'block' => [ 2 ] ], + [] + ], + [ + 'Basic test for "delete" action.', + [ 2, 3 ], + [ + 'delete', + 'Test page' + ], + [ 'degroup' => [ 3 ] ], + [] + ], + [ + 'Basic test for "createaccount" action.', + [ 1, 2, 3, 4 ], + [ + 'createaccount', + null, + 'username' => 'AnotherUser' + ], + [], + [] + ], + [ + 'Test to check that all tags are applied.', + [ 5 ], + [ + 'edit', + 'User:FilteredUser', + 'oldText' => 'Hey.', + 'newText' => 'I am a very nice user, really!', + 'summary' => '' + ], + [ 'tag' => [ 5 ] ], + [] + ], + [ + 'Test to check that the edit is disallowed.', + [ 6 ], + [ + 'edit', + 'Help:Help', + 'oldText' => 'Some help.', + 'newText' => 'Some help for you', + 'summary' => 'Help! I need somebody' + ], + [ 'disallow' => [ 6 ] ], + [] + ], + [ + 'Test to check that degroup and block are executed together.', + [ 2, 3, 7, 8 ], + [ + 'edit', + 'Link', + 'oldText' => 'What is a link?', + 'newText' => 'A link is something like this: [[Link|]].', + 'summary' => 'Explaining' + ], + [ 'degroup' => [ 7 ], 'block' => [ 8 ] ], + [] + ], + [ + 'Test to check that the block duration is the longest one.', + [ 8, 9 ], + [ + 'edit', + 'Whatever', + 'oldText' => 'Whatever is whatever', + 'newText' => 'Whatever is whatever, whatever it is. BTW, here is a [[Link|]]', + 'summary' => 'Whatever' + ], + [ 'disallow' => [ 8 ], 'block' => [ 8 ] ], + [] + ], + [ + 'Test to check that throttled filters only execute "safe" actions.', + [ 10 ], + [ + 'edit', + 'Buffalo', + 'oldText' => 'Buffalo', + 'newText' => 'Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.', + 'summary' => 'Buffalo!' + ], + [ 'tag' => [ 10 ] ], + [] + ], + [ + 'Test to see that throttling works well.', + [ 11 ], + [ + 'edit', + 'Throttle', + 'oldText' => 'What is throttle?', + 'firstNewText' => 'Throttle is something that should happen...', + 'secondNewText' => '... Right now!', + 'summary' => 'Throttle' + ], + [ 'throttle' => [ 11 ], 'disallow' => [ 11 ] ], + [ 'makeGoodEditFirst' ] + ], + [ + 'Test to check that degroup and block are both executed and degroup warning is shown twice.', + [ 1, 3, 7, 12 ], + [ + 'edit', + 'User:FilteredUser', + 'oldText' => '', + 'newText' => 'A couple of lines about me...', + 'summary' => 'My user page' + ], + [ 'block' => [ 12 ], 'degroup' => [ 7, 12 ] ], + [] + ], + [ + 'Test to check that every throttled filter only executes "safe" actions.', + [ 10, 13 ], + [ + 'edit', + 'Tyger! Tyger! Burning bright', + 'oldText' => 'In the forests of the night', + 'newText' => 'What immortal hand or eye', + 'summary' => 'Could frame thy fearful symmetry?' + ], + [ 'tag' => [ 10 ] ], + [] + ], + [ + 'Test to check that runtime exceptions (division by zero) are correctly handled.', + [ 14 ], + [ + 'edit', + '0', + 'oldText' => 'Old text', + 'newText' => 'New text', + 'summary' => 'Some summary' + ], + [], + [] + ], + [ + 'Test to check that the conditions limit works.', + [ 8, 10 ], + [ + 'edit', + 'Anything', + 'oldText' => 'Bar', + 'newText' => 'Foo', + 'summary' => '' + ], + [], + [ 'hitCondsLimit' ] + ], + [ + 'Test slow executions.', + [ 7, 12 ], + [ + 'edit', + 'Something', + 'oldText' => 'Please allow me', + 'newText' => 'to introduce myself', + 'summary' => '' + ], + [ 'degroup' => [ 7 ] ], + [ 'hitTimeLimit' ] + ], + [ + 'Test throttling a dangerous filter.', + [ 13 ], + [ + 'edit', + 'My page', + 'oldText' => '', + 'newText' => 'AbuseFilter will not block me', + 'summary' => '' + ], + [], + [ 'hitThrottleLimit' ] + ], + ]; + } +} diff --git a/AbuseFilter/tests/phpunit/AbuseFilterParserTest.php b/AbuseFilter/tests/phpunit/AbuseFilterParserTest.php new file mode 100644 index 00000000..747e500a --- /dev/null +++ b/AbuseFilter/tests/phpunit/AbuseFilterParserTest.php @@ -0,0 +1,744 @@ +<?php +/** + * Tests for the AbuseFilter parser + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GPL-2.0-or-later + * @author Marius Hoch < hoo@online.de > + */ + +/** + * @group Test + * @group AbuseFilter + * @group AbuseFilterParser + * + * @covers AbuseFilterCachingParser + * @covers AFPTreeParser + * @covers AFPTreeNode + * @covers AFPParserState + * @covers AbuseFilterParser + * @covers AbuseFilterTokenizer + * @covers AFPToken + * @covers AFPUserVisibleException + * @covers AFPException + * @covers AFPData + * @covers AbuseFilterVariableHolder + * @covers AFComputedVariable + */ +class AbuseFilterParserTest extends MediaWikiTestCase { + /** + * @return AbuseFilterParser + */ + public static function getParser() { + static $parser = null; + if ( !$parser ) { + $parser = new AbuseFilterParser(); + } else { + $parser->resetState(); + } + return $parser; + } + + /** + * @return AbuseFilterParser[] + */ + public static function getParsers() { + static $parsers = null; + if ( !$parsers ) { + $parsers = [ + new AbuseFilterParser() + // @ToDo: Here we should also instantiate an AbuseFilterCachingParser as we'll have + // fixed its problems (T156095). Right now it may break otherwise working tests (see T201193) + ]; + } + return $parsers; + } + + /** + * @dataProvider readTests + */ + public function testParser( $testName, $rule, $expected ) { + foreach ( self::getParsers() as $parser ) { + $actual = $parser->parse( $rule ); + $this->assertEquals( $expected, $actual, 'Running parser test ' . $testName ); + } + } + + /** + * @return array + */ + public function readTests() { + $tests = []; + $testPath = __DIR__ . "/../parserTests"; + $testFiles = glob( $testPath . "/*.t" ); + + foreach ( $testFiles as $testFile ) { + $testName = substr( $testFile, 0, -2 ); + + $resultFile = $testName . '.r'; + $rule = trim( file_get_contents( $testFile ) ); + $result = trim( file_get_contents( $resultFile ) ) === 'MATCH'; + + $tests[] = [ + basename( $testName ), + $rule, + $result + ]; + } + + return $tests; + } + + /** + * Test expression evaluation + * + * @dataProvider provideExpressions + */ + public function testEvaluateExpression( $expr, $expected ) { + foreach ( self::getParsers() as $parser ) { + $actual = $parser->evaluateExpression( $expr ); + $this->assertEquals( $expected, $actual ); + } + } + + /** + * Data provider for testEvaluateExpression + * + * @return array + */ + public function provideExpressions() { + return [ + [ '1 === 1', true ], + [ 'rescape( "abc* (def)" )', 'abc\* \(def\)' ], + [ 'str_replace( "foobarbaz", "bar", "-" )', 'foo-baz' ], + [ 'rmdoubles( "foobybboo" )', 'fobybo' ], + [ 'lcase("FÁmí")', 'fámí' ], + [ 'substr( "foobar", 0, 3 )', 'foo' ] + ]; + } + + /** + * Ensure that AbuseFilterTokenizer::OPERATOR_RE matches the contents + * and order of AbuseFilterTokenizer::$operators. + */ + public function testOperatorRe() { + $operatorRe = '/(' . implode( '|', array_map( function ( $op ) { + return preg_quote( $op, '/' ); + }, AbuseFilterTokenizer::$operators ) ) . ')/A'; + $this->assertEquals( $operatorRe, AbuseFilterTokenizer::OPERATOR_RE ); + } + + /** + * Ensure that AbuseFilterTokenizer::RADIX_RE matches the contents + * and order of AbuseFilterTokenizer::$bases. + */ + public function testRadixRe() { + $baseClass = implode( '', array_keys( AbuseFilterTokenizer::$bases ) ); + $radixRe = "/([0-9A-Fa-f]+(?:\.\d*)?|\.\d+)([$baseClass])?/Au"; + $this->assertEquals( $radixRe, AbuseFilterTokenizer::RADIX_RE ); + } + + /** + * Ensure the number of conditions counted for given expressions is right. + * + * @dataProvider condCountCases + */ + public function testCondCount( $rule, $expected ) { + $parser = self::getParser(); + $countBefore = AbuseFilter::$condCount; + $parser->parse( $rule ); + $countAfter = AbuseFilter::$condCount; + $actual = $countAfter - $countBefore; + $this->assertEquals( $expected, $actual, 'Condition count for ' . $rule ); + } + + /** + * Data provider for testCondCount method. + * @return array + */ + public function condCountCases() { + return [ + [ '((("a" == "b")))', 1 ], + [ 'contains_any("a", "b", "c")', 1 ], + [ '"a" == "b" == "c"', 2 ], + [ '"a" in "b" + "c" in "d" + "e" in "f"', 3 ], + [ 'true', 0 ], + [ '"a" == "a" | "c" == "d"', 1 ], + [ '"a" == "b" & "c" == "d"', 1 ], + ]; + } + + /** + * Ensure get_matches function captures returns expected output. + * @param string $needle Regex to pass to get_matches. + * @param string $haystack String to run regex against. + * @param string[] $expected The expected values of the matched groups. + * @covers AbuseFilterParser::funcGetMatches + * @dataProvider getMatchesCases + */ + public function testGetMatches( $needle, $haystack, $expected ) { + $parser = self::getParser(); + $afpData = $parser->intEval( "get_matches('$needle', '$haystack')" )->data; + + // Extract matches from AFPData. + $matches = array_map( function ( $afpDatum ) { + return $afpDatum->data; + }, $afpData ); + + $this->assertEquals( $expected, $matches ); + } + + /** + * Data provider for get_matches method. + * @return array + */ + public function getMatchesCases() { + return [ + [ + 'You say (.*) \(and I say (.*)\)\.', + 'You say hello (and I say goodbye).', + [ + 'You say hello (and I say goodbye).', + 'hello', + 'goodbye', + ], + ], + [ + 'I(?: am)? the ((walrus|egg man).*)\!', + 'I am the egg man, I am the walrus !', + [ + 'I am the egg man, I am the walrus !', + 'egg man, I am the walrus ', + 'egg man', + ], + ], + [ + 'this (does) not match', + 'foo bar', + [ + false, + false, + ], + ], + ]; + } + + /** + * Base method for testing exceptions + * + * @param string $excep Identifier of the exception (e.g. 'unexpectedtoken') + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + */ + private function exceptionTest( $excep, $expr, $caller ) { + $parser = self::getParser(); + try { + $parser->parse( $expr ); + } catch ( AFPUserVisibleException $e ) { + $this->assertEquals( + $excep, + $e->mExceptionID, + "Exception $excep not thrown in AbuseFilterParser::$caller" + ); + return; + } + + $this->fail( "Exception $excep not thrown in AbuseFilterParser::$caller" ); + } + + /** + * Test the 'expectednotfound' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelSet + * @covers AbuseFilterParser::doLevelConditions + * @covers AbuseFilterParser::doLevelBraces + * @covers AbuseFilterParser::doLevelFunction + * @covers AbuseFilterParser::doLevelAtom + * @covers AbuseFilterParser::skipOverBraces + * @covers AbuseFilterParser::doLevelArrayElements + * @dataProvider expectedNotFound + */ + public function testExpectedNotFoundException( $expr, $caller ) { + $this->exceptionTest( 'expectednotfound', $expr, $caller ); + } + + /** + * Data provider for testExpectedNotFoundException. + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function expectedNotFound() { + return [ + [ 'a:= [1,2,3]; a[1 = 4', 'doLevelSet' ], + [ "if 1 = 1 'foo'", 'doLevelConditions' ], + [ "if 1 = 1 then 'foo'", 'doLevelConditions' ], + [ "if 1 = 1 then 'foo' else 'bar'", 'doLevelConditions' ], + [ "a := 1 = 1 ? 'foo'", 'doLevelConditions' ], + [ '(1 = 1', 'doLevelBraces' ], + [ 'lcase = 3', 'doLevelFunction' ], + [ 'lcase( 3 = 1', 'doLevelFunction' ], + [ 'a := [1,2', 'doLevelAtom' ], + [ '1 = 1 | (', 'skipOverBraces' ], + [ 'a := [1,2,3]; 3 = a[5', 'doLevelArrayElements' ], + ]; + } + + /** + * Test the 'unexpectedatend' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelEntry + * @dataProvider unexpectedAtEnd + */ + public function testUnexpectedAtEndException( $expr, $caller ) { + $this->exceptionTest( 'unexpectedatend', $expr, $caller ); + } + + /** + * Data provider for testUnexpectedAtEndException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unexpectedAtEnd() { + return [ + [ "'a' = 1 )", 'doLevelEntry' ], + ]; + } + + /** + * Test the 'unrecognisedvar' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelSet + * @covers AbuseFilterParser::getVarValue + * @dataProvider unrecognisedVar + */ + public function testUnrecognisedVarException( $expr, $caller ) { + $this->exceptionTest( 'unrecognisedvar', $expr, $caller ); + } + + /** + * Data provider for testUnrecognisedVarException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unrecognisedVar() { + return [ + [ 'a[1] := 5', 'doLevelSet' ], + [ 'a = 5', 'getVarValue' ], + ]; + } + + /** + * Test the 'notarray' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelSet + * @covers AbuseFilterParser::doLevelArrayElements + * @dataProvider notArray + */ + public function testNotArrayException( $expr, $caller ) { + $this->exceptionTest( 'notarray', $expr, $caller ); + } + + /** + * Data provider for testNotArrayException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function notArray() { + return [ + [ 'a := 5; a[1] = 5', 'doLevelSet' ], + [ 'a := 1; 3 = a[5]', 'doLevelArrayElements' ], + ]; + } + + /** + * Test the 'outofbounds' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelSet + * @covers AbuseFilterParser::doLevelArrayElements + * @dataProvider outOfBounds + */ + public function testOutOfBoundsException( $expr, $caller ) { + $this->exceptionTest( 'outofbounds', $expr, $caller ); + } + + /** + * Data provider for testOutOfBoundsException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function outOfBounds() { + return [ + [ 'a := [2]; a[5] = 9', 'doLevelSet' ], + [ 'a := [1,2,3]; 3 = a[5]', 'doLevelArrayElements' ], + ]; + } + + /** + * Test the 'unrecognisedkeyword' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelAtom + * @dataProvider unrecognisedKeyword + */ + public function testUnrecognisedKeywordException( $expr, $caller ) { + $this->exceptionTest( 'unrecognisedkeyword', $expr, $caller ); + } + + /** + * Data provider for testUnrecognisedKeywordException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unrecognisedKeyword() { + return [ + [ '5 = rlike', 'doLevelAtom' ], + ]; + } + + /** + * Test the 'unexpectedtoken' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::doLevelAtom + * @dataProvider unexpectedToken + */ + public function testUnexpectedTokenException( $expr, $caller ) { + $this->exceptionTest( 'unexpectedtoken', $expr, $caller ); + } + + /** + * Data provider for testUnexpectedTokenException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unexpectedToken() { + return [ + [ '1 =? 1', 'doLevelAtom' ], + ]; + } + + /** + * Test the 'disabledvar' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::getVarValue + * @dataProvider disabledVar + */ + public function testDisabledVarException( $expr, $caller ) { + $this->exceptionTest( 'disabledvar', $expr, $caller ); + } + + /** + * Data provider for testDisabledVarException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function disabledVar() { + return [ + [ 'old_text = 1', 'getVarValue' ], + ]; + } + + /** + * Test the 'overridebuiltin' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::setUserVariable + * @dataProvider overrideBuiltin + */ + public function testOverrideBuiltinException( $expr, $caller ) { + $this->exceptionTest( 'overridebuiltin', $expr, $caller ); + } + + /** + * Data provider for testOverrideBuiltinException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function overrideBuiltin() { + return [ + [ 'added_lines := 1', 'setUserVariable' ], + ]; + } + + /** + * Test the 'regexfailure' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::funcRCount + * @covers AbuseFilterParser::funcGetMatches + * @dataProvider regexFailure + */ + public function testRegexFailureException( $expr, $caller ) { + $this->exceptionTest( 'regexfailure', $expr, $caller ); + } + + /** + * Data provider for testRegexFailureException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function regexFailure() { + return [ + [ "rcount('(','a')", 'funcRCount' ], + [ "get_matches('this (should fail', 'any haystack')", 'funcGetMatches' ], + ]; + } + + /** + * Test the 'invalidiprange' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterParser::funcIPInRange + * @dataProvider invalidIPRange + */ + public function testInvalidIPRangeException( $expr, $caller ) { + $this->exceptionTest( 'invalidiprange', $expr, $caller ); + } + + /** + * Data provider for testInvalidIPRangeException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function invalidIPRange() { + return [ + [ "ip_in_range('0.0.0.0', 'lol')", 'funcIPInRange' ], + ]; + } + + /** + * Test functions which take exactly one parameters calling them + * without 0 params. They should throw a 'noparams' exception. + * + * @param string $func The function to test + * @covers AbuseFilterParser::funcLc + * @covers AbuseFilterParser::funcUc + * @covers AbuseFilterParser::funcLen + * @covers AbuseFilterParser::funcSpecialRatio + * @covers AbuseFilterParser::funcCount + * @covers AbuseFilterParser::funcRCount + * @covers AbuseFilterParser::funcCCNorm + * @covers AbuseFilterParser::funcSanitize + * @covers AbuseFilterParser::funcRMSpecials + * @covers AbuseFilterParser::funcRMWhitespace + * @covers AbuseFilterParser::funcRMDoubles + * @covers AbuseFilterParser::funcNorm + * @covers AbuseFilterParser::funcStrRegexEscape + * @covers AbuseFilterParser::castString + * @covers AbuseFilterParser::castInt + * @covers AbuseFilterParser::castFloat + * @covers AbuseFilterParser::castBool + * @dataProvider oneParamFuncs + * @expectedException AFPUserVisibleException + * @expectedExceptionMessageRegExp /^No parameters given to function/ + */ + public function testNoParamsException( $func ) { + $parser = self::getParser(); + $parser->parse( "$func()" ); + } + + /** + * Data provider for testNoParamsException, returns a list of + * functions taking a single parameter + * + * @return array + */ + public function oneParamFuncs() { + return [ + [ 'lcase' ], + [ 'ucase' ], + [ 'length' ], + [ 'strlen' ], + [ 'specialratio' ], + [ 'count' ], + [ 'rcount' ], + [ 'ccnorm' ], + [ 'sanitize' ], + [ 'rmspecials' ], + [ 'rmwhitespace' ], + [ 'rmdoubles' ], + [ 'norm' ], + [ 'rescape' ], + [ 'string' ], + [ 'int' ], + [ 'float' ], + [ 'bool' ], + ]; + } + + /** + * Test functions taking two parameters by providing only one. + * They should throw a 'notenoughargs' exception. + * + * @param string $func The function to test + * @covers AbuseFilterParser::funcGetMatches + * @covers AbuseFilterParser::funcIPInRange + * @covers AbuseFilterParser::funcContainsAny + * @covers AbuseFilterParser::funcContainsAll + * @covers AbuseFilterParser::funcCCNormContainsAny + * @covers AbuseFilterParser::funcCCNormContainsAll + * @covers AbuseFilterParser::funcEqualsToAny + * @covers AbuseFilterParser::funcSubstr + * @covers AbuseFilterParser::funcStrPos + * @covers AbuseFilterParser::funcSetVar + * @dataProvider twoParamsFuncs + * @expectedException AFPUserVisibleException + * @expectedExceptionMessageRegExp /^Not enough arguments to function [^ ]+ called at character \d+.\nExpected 2 arguments, got 1/ + */ + public function testNotEnoughArgsExceptionTwo( $func ) { + $parser = self::getParser(); + // Nevermind if the argument can't be string since we check the amount + // of parameters before anything else. + $parser->parse( "$func('foo')" ); + } + + /** + * Data provider for testNotEnoughArgsExceptionTwo, returns the list of + * functions taking two parameters. + * + * @return array + */ + public function twoParamsFuncs() { + return [ + [ 'get_matches' ], + [ 'ip_in_range' ], + [ 'contains_any' ], + [ 'contains_all' ], + [ 'ccnorm_contains_any' ], + [ 'ccnorm_contains_all' ], + [ 'equals_to_any' ], + [ 'substr' ], + [ 'strpos' ], + [ 'set_var' ], + ]; + } + + /** + * Test functions taking three parameters by providing only two. + * They should throw a 'notenoughargs' exception. + * + * @param string $func The function to test + * @covers AbuseFilterParser::funcStrReplace + * @dataProvider threeParamsFuncs + * @expectedException AFPUserVisibleException + * @expectedExceptionMessageRegExp /^Not enough arguments to function [^ ]+ called at character \d+.\nExpected 3 arguments, got 2/ + */ + public function testNotEnoughArgsExceptionThree( $func ) { + $parser = self::getParser(); + // Nevermind if the argument can't be string since we check the amount + // of parameters before anything else. + $parser->parse( "$func('foo', 'bar')" ); + } + + /** + * Data provider for testNotEnoughArgsExceptionThree, returns the list of + * functions taking three parameters. + * + * @return array + */ + public function threeParamsFuncs() { + return [ + [ 'str_replace' ], + ]; + } + + /** + * Check that deprecated variables are correctly translated to the new ones with a debug notice + * + * @param string $old The old name of the variable + * @param string $new The new name of the variable + * @dataProvider provideDeprecatedVars + */ + public function testDeprecatedVars( $old, $new ) { + $loggerMock = new TestLogger(); + $loggerMock->setCollect( true ); + $this->setLogger( 'AbuseFilterDeprecatedVars', $loggerMock ); + + $parser = self::getParser(); + $actual = $parser->parse( "$old === $new" ); + + $loggerBuffer = $loggerMock->getBuffer(); + // Check that the use has been logged + $found = false; + foreach ( $loggerBuffer as $entry ) { + $check = preg_match( '/AbuseFilter: deprecated variable/', $entry[1] ); + if ( $check ) { + $found = true; + break; + } + } + if ( !$found ) { + $this->fail( "The use of the deprecated variable $old was not logged." ); + } + + $this->assertTrue( $actual, "AbuseFilter deprecated variable $old is not parsed correctly" ); + } + + /** + * Data provider for testDeprecatedVars + * @return array + */ + public function provideDeprecatedVars() { + $deprecated = AbuseFilter::$deprecatedVars; + $data = []; + foreach ( $deprecated as $old => $new ) { + $data[] = [ $old, $new ]; + } + return $data; + } +} diff --git a/AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php b/AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php new file mode 100644 index 00000000..6a5b3121 --- /dev/null +++ b/AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php @@ -0,0 +1,596 @@ +<?php +/** + * Tests for validating and saving a filter + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GPL-2.0-or-later + */ + +/** + * @group Test + * @group AbuseFilter + * @group AbuseFilterSave + * @group Database + * + * @covers AbuseFilter + * @covers AbuseFilterViewEdit + * @covers AbuseFilterParser + */ +class AbuseFilterSaveTest extends MediaWikiTestCase { + protected static $mUser, $mParameters; + + /** + * @var array This tables will be deleted in parent::tearDown + */ + protected $tablesUsed = [ + 'abuse_filter', + 'abuse_filter_action', + 'abuse_filter_history', + 'abuse_filter_log' + ]; + + /** + * @see MediaWikiTestCase::setUp + */ + protected function setUp() { + parent::setUp(); + $user = User::newFromName( 'FilterTester' ); + $user->addToDatabase(); + $user->addGroup( 'filterEditor' ); + RequestContext::getMain()->setUser( $user ); + self::$mUser = $user; + // Make sure that the config we're using is the one we're expecting + $this->setMwGlobals( [ + 'wgUser' => $user, + 'wgAbuseFilterRestrictions' => [ + 'degroup' => true + ], + 'wgAbuseFilterIsCentral' => true, + 'wgAbuseFilterActions' => [ + 'throttle' => true, + 'warn' => true, + 'disallow' => true, + 'blockautopromote' => true, + 'block' => true, + 'rangeblock' => true, + 'degroup' => true, + 'tag' => true + ], + 'wgAbuseFilterValidGroups' => [ + 'default', + 'flow' + ] + ] ); + $this->setGroupPermissions( [ + 'filterEditor' => [ + 'abusefilter-modify' => true, + 'abusefilter-modify-restricted' => false, + 'abusefilter-modify-global' => false, + ], + 'filterEditorGlobal' => [ + 'abusefilter-modify' => true, + 'abusefilter-modify-global' => true, + ] + ] ); + } + + /** + * Gets an instance of AbuseFilterViewEdit ready for creating or editing filter + * + * @param string $filter 'new' for a new filter, its ID otherwise + * @return AbuseFilterViewEdit + */ + private static function getViewEdit( $filter ) { + $special = new SpecialAbuseFilter(); + $context = RequestContext::getMain( self::getRequest() ); + $context->setRequest( self::getRequest() ); + + $special->setContext( $context ); + $special->mFilter = $filter; + $viewEdit = new AbuseFilterViewEdit( $special, [ $filter ] ); + // Being a static property, it's not deleted between tests + $viewEdit::$mLoadedRow = null; + + return $viewEdit; + } + + /** + * Creates a FauxRequest object + * + * @return FauxRequest + */ + private static function getRequest() { + $params = [ + 'wpFilterRules' => self::$mParameters['rules'], + 'wpFilterDescription' => self::$mParameters['description'], + 'wpFilterNotes' => self::$mParameters['notes'], + 'wpFilterGroup' => self::$mParameters['group'], + 'wpFilterEnabled' => self::$mParameters['enabled'], + 'wpFilterHidden' => self::$mParameters['hidden'], + 'wpFilterDeleted' => self::$mParameters['deleted'], + 'wpFilterGlobal' => self::$mParameters['global'], + 'wpFilterActionThrottle' => self::$mParameters['throttleEnabled'], + 'wpFilterThrottleCount' => self::$mParameters['throttleCount'], + 'wpFilterThrottlePeriod' => self::$mParameters['throttlePeriod'], + 'wpFilterThrottleGroups' => self::$mParameters['throttleGroups'], + 'wpFilterActionWarn' => self::$mParameters['warnEnabled'], + 'wpFilterWarnMessage' => self::$mParameters['warnMessage'], + 'wpFilterWarnMessageOther' => self::$mParameters['warnMessageOther'], + 'wpFilterActionDisallow' => self::$mParameters['disallowEnabled'], + 'wpFilterActionBlockautopromote' => self::$mParameters['blockautopromoteEnabled'], + 'wpFilterActionDegroup' => self::$mParameters['degroupEnabled'], + 'wpFilterActionBlock' => self::$mParameters['blockEnabled'], + 'wpFilterBlockTalk' => self::$mParameters['blockTalk'], + 'wpBlockAnonDuration' => self::$mParameters['blockAnons'], + 'wpBlockUserDuration' => self::$mParameters['blockUsers'], + 'wpFilterActionRangeblock' => self::$mParameters['rangeblockEnabled'], + 'wpFilterActionTag' => self::$mParameters['tagEnabled'], + 'wpFilterTags' => self::$mParameters['tagTags'], + ]; + + // Checkboxes aren't included at all if they aren't selected. We can remove them + // this way (instead of iterating a hardcoded list) since they're the only false values + $params = array_filter( $params, function ( $el ) { + return $el !== false; + } ); + + $request = new FauxRequest( $params, true ); + return $request; + } + + /** + * Creates $amount new filters, in case we need to test updating an existing one + * + * @param int $amount How many filters to create + */ + private static function createNewFilters( $amount ) { + $defaultRow = [ + 'af_pattern' => '/**/', + 'af_user' => 0, + 'af_user_text' => 'FilterTester', + 'af_timestamp' => wfTimestampNow(), + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Mock filter', + 'af_hidden' => 0, + 'af_hit_count' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_actions' => '', + 'af_global' => 0, + 'af_group' => 'default' + ]; + + $dbw = wfGetDB( DB_MASTER ); + for ( $i = 1; $i <= $amount; $i++ ) { + $dbw->replace( + 'abuse_filter', + [ 'af_id' ], + $defaultRow, + __METHOD__ + ); + } + } + + /** + * Validate and save a filter given its parameters + * + * @param array $args Parameters of the filter and metadata for the test + * @covers AbuseFilter::saveFilter + * @dataProvider provideFilters + */ + public function testSaveFilter( $args ) { + // Preliminar stuff for the test + if ( $args['testData']['customUserGroup'] ) { + self::$mUser->addGroup( $args['testData']['customUserGroup'] ); + } + + if ( $args['testData']['needsOtherFilters'] ) { + self::createNewFilters( $args['testData']['needsOtherFilters'] ); + } + + $fixedParameters = [ + 'id' => 'new', + 'notes' => '', + 'group' => 'default', + 'enabled' => true, + 'hidden' => false, + 'global' => false, + 'deleted' => false, + 'throttled' => 0, + 'throttleEnabled' => false, + 'throttleCount' => 0, + 'throttlePeriod' => 0, + 'throttleGroups' => '', + 'warnEnabled' => false, + 'warnMessage' => 'abusefilter-warning', + 'warnMessageOther' => '', + 'disallowEnabled' => false, + 'blockautopromoteEnabled' => false, + 'degroupEnabled' => false, + 'blockEnabled' => false, + 'blockTalk' => false, + 'blockAnons' => 'infinity', + 'blockUsers' => 'infinity', + 'rangeblockEnabled' => false, + 'tagEnabled' => false, + 'tagTags' => '' + ]; + + // Extract parameters from testset and build what we need to save a filter + // The values specified in the testset will overwrite the fixed ones. + self::$mParameters = $args['filterParameters'] + $fixedParameters; + $filter = self::$mParameters['id']; + $viewEdit = self::getViewEdit( $filter ); + $request = self::getRequest(); + list( $newRow, $actions ) = $viewEdit->loadRequest( $filter ); + self::$mParameters['rowActions'] = implode( ',', array_keys( array_filter( $actions ) ) ); + + // Send data for validation and saving + $status = AbuseFilter::saveFilter( $viewEdit, $filter, $request, $newRow, $actions ); + + // Must be removed for the next test + if ( $args['testData']['customUserGroup'] ) { + self::$mUser->removeGroup( $args['testData']['customUserGroup'] ); + } + + $shouldFail = $args['testData']['shouldFail']; + $shouldBeSaved = $args['testData']['shouldBeSaved']; + $furtherInfo = null; + $expected = true; + if ( $shouldFail ) { + if ( $status->isGood() ) { + $furtherInfo = 'The filter validation returned a valid status.'; + $result = false; + } else { + $result = $status->getErrors(); + $result = $result[0]['message']; + $expected = $args['testData']['expectedMessage']; + } + } else { + if ( $shouldBeSaved ) { + $value = $status->getValue(); + $result = $status->isGood() && is_array( $value ) && count( $value ) === 2 && + is_numeric( $value[0] ) && is_numeric( $value[1] ); + } else { + $result = $status->isGood() && $status->getValue() === false; + } + } + + $errorMessage = $args['testData']['doingWhat'] . '. Expected: ' . + $args['testData']['expectedResult'] . '.'; + if ( $furtherInfo ) { + $errorMessage .= "\nFurther info: " . $furtherInfo; + } + $this->assertEquals( + $expected, + $result, + $errorMessage + ); + } + + /** + * Data provider for creating and editing filters. + * @return array + */ + public function provideFilters() { + return [ + [ + [ + 'filterParameters' => [ + 'rules' => '', + 'description' => '', + 'blockautopromoteEnabled' => true, + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter without description and rules', + 'expectedResult' => 'a "missing required fields" error message', + 'expectedMessage' => 'abusefilter-edit-missingfields', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '/* My rules */', + 'description' => 'Some new filter', + 'enabled' => false, + 'deleted' => true, + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter with only rules and description', + 'expectedResult' => 'the saving to be successful', + 'expectedMessage' => '', + 'shouldFail' => false, + 'shouldBeSaved' => true, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => 'rlike', + 'description' => 'This syntax aint good', + 'blockEnabled' => true, + 'blockTalk' => true, + 'blockAnons' => '8 hours', + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter with wrong syntax', + 'expectedResult' => 'a "wrong syntax" error message', + 'expectedMessage' => 'abusefilter-edit-badsyntax', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Enabled and deleted', + 'deleted' => true, + 'blockEnabled' => true, + 'blockTalk' => true, + 'blockAnons' => '8 hours', + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter marking it both enabled and deleted', + 'expectedResult' => 'an error message', + 'expectedMessage' => 'abusefilter-edit-deleting-enabled', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Reserved tag', + 'notes' => 'Some notes', + 'hidden' => true, + 'tagEnabled' => true, + 'tagTags' => 'mw-undo' + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter with a reserved tag', + 'expectedResult' => 'an error message saying that the tag cannot be used', + 'expectedMessage' => 'abusefilter-edit-bad-tags', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Invalid tag', + 'notes' => 'Some notes', + 'tagEnabled' => true, + 'tagTags' => 'some|tag' + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter with an invalid tag', + 'expectedResult' => 'an error message saying that the tag cannot be used', + 'expectedMessage' => 'tags-create-invalid-chars', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Global without perms', + 'global' => true, + 'disallowEnabled' => true, + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a global filter without enough rights', + 'expectedResult' => 'an error message saying that I do not have the required rights', + 'expectedMessage' => 'abusefilter-edit-notallowed-global', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Global with invalid warn message', + 'global' => true, + 'warnEnabled' => true, + 'warnMessage' => 'abusefilter-beautiful-warning', + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a global filter with a custom warn message', + 'expectedResult' => 'an error message saying that custom warn messages ' . + 'cannot be used for global rules', + 'expectedMessage' => 'abusefilter-edit-notallowed-global-custom-msg', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => 'filterEditorGlobal', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Restricted action', + 'degroupEnabled' => true, + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter with a restricted action', + 'expectedResult' => 'an error message saying that the action is restricted', + 'expectedMessage' => 'abusefilter-edit-restricted', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'id' => '1', + 'rules' => '/**/', + 'description' => 'Mock filter', + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter without changing anything', + 'expectedResult' => 'the validation to pass without the filter being saved', + 'expectedMessage' => '', + 'shouldFail' => false, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => 1 + ] + ] + ], + [ + [ + 'filterParameters' => [ + 'rules' => '1==1', + 'description' => 'Invalid throttle groups', + 'notes' => 'Throttle... Again', + 'throttleEnabled' => true, + 'throttleCount' => 11, + 'throttlePeriod' => 111, + 'throttleGroups' => 'user\nfoo' + ], + 'testData' => [ + 'doingWhat' => 'Trying to save a filter with invalid throttle groups', + 'expectedResult' => 'an error message saying that throttle groups are invalid', + 'expectedMessage' => 'abusefilter-edit-invalid-throttlegroups', + 'shouldFail' => true, + 'shouldBeSaved' => false, + 'customUserGroup' => '', + 'needsOtherFilters' => false + ] + ] + ] + ]; + } + + /** + * Check that our tag validation is working properly. Note that we only need one test + * for each called function. Consistency within ChangeTags functions should be + * assured by tests in core. The test for canAddTagsAccompanyingChange and canCreateTag + * are missing because they won't actually fail, never. Resolving T173917 would + * greatly improve the situation and could help writing better tests. + * + * @param string $tag The tag to validate + * @param string|null $error The expected error message. Null if validations should pass + * @covers AbuseFilter::isAllowedTag + * @dataProvider provideTags + */ + public function testIsAllowedTag( $tag, $error ) { + $status = AbuseFilter::isAllowedTag( $tag ); + + if ( !$status->isGood() ) { + $actualError = $status->getErrors(); + $actualError = $actualError[0]['message']; + } else { + $actualError = null; + if ( $error !== null ) { + $this->fail( "Tag validation returned a valid status instead of the expected '$error' error." ); + } + } + + $this->assertSame( + $error, + $actualError, + "Expected message '$error', got '$actualError' while validating the tag '$tag'." + ); + } + + /** + * Data provider for testIsAllowedTag + * @return array + */ + public function provideTags() { + return [ + [ 'a|b', 'tags-create-invalid-chars' ], + [ 'mw-undo', 'abusefilter-edit-bad-tags' ], + [ 'abusefilter-condition-limit', 'abusefilter-tag-reserved' ], + [ 'my_tag', null ], + ]; + } + + /** + * Check that throttle parameters validation works fine + * + * @param array $params Throttle parameters + * @param string|null $error The expected error message. Null if validations should pass + * @covers AbuseFilter::checkThrottleParameters + * @dataProvider provideThrottleParameters + */ + public function testCheckThrottleParameters( $params, $error ) { + $result = AbuseFilter::checkThrottleParameters( $params ); + $this->assertSame( $error, $result, 'Throttle parameter validation does not work as expected.' ); + } + + /** + * Data provider for testCheckThrottleParameters + * @return array + */ + public function provideThrottleParameters() { + return [ + [ [ '1', '5,23', 'user', 'ip', 'page,range', 'ip,user', 'range,ip' ], null ], + [ [ '1', '5.3,23', 'user', 'ip' ], 'abusefilter-edit-invalid-throttlecount' ], + [ [ '1', '-3,23', 'user', 'ip' ], 'abusefilter-edit-invalid-throttlecount' ], + [ [ '1', '5,2.3', 'user', 'ip' ], 'abusefilter-edit-invalid-throttleperiod' ], + [ [ '1', '4,-14', 'user', 'ip' ], 'abusefilter-edit-invalid-throttleperiod' ], + [ [ '1', '3,33' ], 'abusefilter-edit-empty-throttlegroups' ], + [ [ '1', '3,33', 'user', 'ip,foo,user' ], 'abusefilter-edit-invalid-throttlegroups' ], + [ [ '1', '3,33', 'foo', 'ip,user' ], 'abusefilter-edit-invalid-throttlegroups' ], + [ [ '1', '3,33', 'foo', 'ip,user,bar' ], 'abusefilter-edit-invalid-throttlegroups' ], + [ [ '1', '3,33', 'user', 'ip,page,user' ], null ], + [ + [ '1', '3,33', 'ip', 'user','user,ip', 'ip,user', 'user,ip,user', 'user', 'ip,ip,user' ], + 'abusefilter-edit-duplicated-throttlegroups' + ], + [ [ '1', '3,33', 'ip,ip,user' ], 'abusefilter-edit-duplicated-throttlegroups' ], + [ [ '1', '3,33', 'user,ip', 'ip,user' ], 'abusefilter-edit-duplicated-throttlegroups' ], + ]; + } +} diff --git a/AbuseFilter/tests/phpunit/AbuseFilterTest.php b/AbuseFilter/tests/phpunit/AbuseFilterTest.php new file mode 100644 index 00000000..926116e2 --- /dev/null +++ b/AbuseFilter/tests/phpunit/AbuseFilterTest.php @@ -0,0 +1,1413 @@ +<?php +/** + * Generic tests for utility functions in AbuseFilter + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GPL-2.0-or-later + */ + +/** + * @group Test + * @group AbuseFilter + * @group AbuseFilterGeneric + * @group Database + * + * @covers AbuseFilter + * @covers AFPData + * @covers AbuseFilterVariableHolder + * @covers AFComputedVariable + */ +class AbuseFilterTest extends MediaWikiTestCase { + /** @var User */ + protected static $mUser; + /** @var Title */ + protected static $mTitle; + /** @var WikiPage */ + protected static $mPage; + /** @var AbuseFilterVariableHolder */ + protected static $mVariables; + + /** + * @var array These tables will be deleted in parent::tearDown. + * We need it to happen to make tests on fresh pages. + */ + protected $tablesUsed = [ + 'page', + 'page_restrictions', + 'abuse_filter', + 'abuse_filter_history', + 'abuse_filter_log', + 'abuse_filter_actions' + ]; + + /** + * @see MediaWikiTestCase::setUp + */ + protected function setUp() { + parent::setUp(); + MWTimestamp::setFakeTime( 1514700000 ); + $user = User::newFromName( 'AnotherFilteredUser' ); + $user->addToDatabase(); + $user->addGroup( 'basicFilteredUser' ); + self::$mUser = $user; + MWTimestamp::setFakeTime( false ); + + self::$mVariables = new AbuseFilterVariableHolder(); + + // Make sure that the config we're using is the one we're expecting + $this->setMwGlobals( [ + 'wgUser' => $user, + 'wgRestrictionTypes' => [ + 'create', + 'edit', + 'move', + 'upload' + ], + 'wgAbuseFilterRestrictions' => [ + 'degroup' => true + ], + 'wgAbuseFilterIsCentral' => true, + 'wgAbuseFilterActions' => [ + 'throttle' => true, + 'warn' => true, + 'disallow' => true, + 'blockautopromote' => true, + 'block' => true, + 'rangeblock' => true, + 'degroup' => true, + 'tag' => true + ], + 'wgAbuseFilterValidGroups' => [ + 'default', + 'flow' + ], + 'wgEnableParserLimitReporting' => false + ] ); + $this->setGroupPermissions( [ + 'basicFilteredUser' => [ + 'abusefilter-view' => true + ], + 'intermediateFilteredUser' => [ + 'abusefilter-log' => true + ], + 'privilegedFilteredUser' => [ + 'abusefilter-private' => true, + 'abusefilter-revert' => true + ] + ] ); + } + + /** + * @see MediaWikiTestCase::tearDown + */ + protected function tearDown() { + MWTimestamp::setFakeTime( false ); + $userGroups = self::$mUser->getGroups(); + // We want to start fresh + foreach ( $userGroups as $group ) { + self::$mUser->removeGroup( $group ); + } + parent::tearDown(); + } + + /** + * Given the name of a variable, naturally sets it to a determined amount + * + * @param string $var The variable name + * @return array the first position is the result (mixed), the second is a boolean + * indicating whether we've been able to compute the given variable + */ + private static function computeExpectedUserVariable( $var ) { + $success = true; + switch ( $var ) { + case 'user_editcount': + // Create a page and make the user edit it 7 times + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( + new WikitextContent( 'AbuseFilter test, page creation' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + for ( $i = 1; $i <= 7; $i++ ) { + $page->doEditContent( + new WikitextContent( "AbuseFilter test, page revision #$i" ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + self::$mUser + ); + } + // Reload to reflect deferred update + self::$mUser->clearInstanceCache(); + $result = 7; + break; + case 'user_name': + $result = self::$mUser->getName(); + break; + case 'user_emailconfirm': + $time = wfTimestampNow(); + self::$mUser->setEmailAuthenticationTimestamp( $time ); + $result = $time; + break; + case 'user_groups': + self::$mUser->addGroup( 'intermediateFilteredUser' ); + $result = self::$mUser->getEffectiveGroups(); + break; + case 'user_rights': + self::$mUser->addGroup( 'privilegedFilteredUser' ); + $result = self::$mUser->getRights(); + break; + case 'user_blocked': + $block = new Block(); + $block->setTarget( self::$mUser ); + $block->setBlocker( 'UTSysop' ); + $block->mReason = 'Testing AbuseFilter variable user_blocked'; + $block->mExpiry = 'infinity'; + + $block->insert(); + $result = true; + break; + default: + $success = false; + $result = null; + } + return [ $result, $success ]; + } + + /** + * Check that the generated user-related variables are correct + * + * @param string $varName The name of the variable we're currently testing + * @covers AbuseFilter::generateUserVars + * @dataProvider provideUserVars + */ + public function testGenerateUserVars( $varName ) { + list( $computed, $successfully ) = self::computeExpectedUserVariable( $varName ); + if ( !$successfully ) { + $this->fail( "Given unknown user-related variable $varName." ); + } + + $variableHolder = AbuseFilter::generateUserVars( self::$mUser ); + $actual = $variableHolder->getVar( $varName )->toNative(); + $this->assertSame( + $computed, + $actual, + "AbuseFilter variable $varName is computed wrongly." + ); + } + + /** + * Data provider for testGenerateUserVars + * @return array + */ + public function provideUserVars() { + return [ + [ 'user_editcount' ], + [ 'user_name' ], + [ 'user_emailconfirm' ], + [ 'user_groups' ], + [ 'user_rights' ], + [ 'user_blocked' ] + ]; + } + + /** + * Check that user_age is correct. Needs a separate function to take into account the + * difference between timestamps due to test execution time + * + * @covers AbuseFilter::generateUserVars + */ + public function testUserAgeVar() { + // Set a fake timestamp so that execution time won't be a problem + MWTimestamp::setFakeTime( 1514700163 ); + $variableHolder = AbuseFilter::generateUserVars( self::$mUser ); + $actual = $variableHolder->getVar( 'user_age' )->toNative(); + + $this->assertEquals( + 163, + $actual, + "AbuseFilter variable user_age is computed wrongly. Expected: 163, actual: $actual." + ); + } + + /** + * Given the name of a variable, naturally sets it to a determined amount + * + * @param string $suffix The suffix of the variable + * @param string|null $options Further options for the test + * @return array the first position is the result (mixed), the second is a boolean + * indicating whether we've been able to compute the given variable. If false, then + * the result may be null if the requested variable doesn't exist, or false if there + * has been some other problem. + */ + private static function computeExpectedTitleVariable( $suffix, $options = null ) { + self::$mTitle = Title::newFromText( 'AbuseFilter test' ); + $page = WikiPage::factory( self::$mTitle ); + + if ( $options === 'restricted' ) { + $action = str_replace( '_restrictions_', '', $suffix ); + $namespace = 0; + if ( $action === 'upload' ) { + // Only files can have it + $namespace = 6; + } + self::$mTitle = Title::makeTitle( $namespace, 'AbuseFilter restrictions test' ); + $page = WikiPage::factory( self::$mTitle ); + if ( $action !== 'create' ) { + // To apply other restrictions, the title has to exist + $page->doEditContent( + new WikitextContent( 'AbuseFilter test for title variables' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + } + $_ = false; + $s = $page->doUpdateRestrictions( + [ $action => true ], + [ $action => 'infinity' ], + $_, + 'Testing restrictions for AbuseFilter', + self::$mUser + ); + } + $success = true; + switch ( $suffix ) { + case '_id': + $result = self::$mTitle->getArticleID(); + break; + case '_namespace': + $result = self::$mTitle->getNamespace(); + break; + case '_title': + $result = self::$mTitle->getText(); + break; + case '_prefixedtitle': + $result = self::$mTitle->getPrefixedText(); + break; + case '_restrictions_create': + $restrictions = self::$mTitle->getRestrictions( 'create' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_restrictions_edit': + $restrictions = self::$mTitle->getRestrictions( 'edit' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_restrictions_move': + $restrictions = self::$mTitle->getRestrictions( 'move' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_restrictions_upload': + $restrictions = self::$mTitle->getRestrictions( 'upload' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_recent_contributors': + // Create the page and make a couple of edits from different users + $page->doEditContent( + new WikitextContent( 'AbuseFilter test for title variables' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + $mockContributors = [ 'Alice', 'Bob', 'Charlie' ]; + foreach ( $mockContributors as $user ) { + $page->doEditContent( + new WikitextContent( "AbuseFilter test, page revision by $user" ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + User::newFromName( $user ) + ); + } + $contributors = array_reverse( $mockContributors ); + array_push( $contributors, self::$mUser->getName() ); + $result = $contributors; + break; + case '_first_contributor': + // Create the page and make a couple of edits from different users + $page->doEditContent( + new WikitextContent( 'AbuseFilter test for title variables' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + $mockContributors = [ 'Alice', 'Bob', 'Charlie' ]; + foreach ( $mockContributors as $user ) { + $page->doEditContent( + new WikitextContent( "AbuseFilter test, page revision by $user" ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + User::newFromName( $user ) + ); + } + $result = self::$mUser->getName(); + break; + default: + $success = false; + $result = null; + } + return [ $result, $success ]; + } + + /** + * Check that the generated title-related variables are correct + * + * @param string $prefix The prefix of the variables we're currently testing + * @param string $suffix The suffix of the variables we're currently testing + * @param string|null $options Whether we want to execute the test with specific options + * Right now, this can only be 'restricted' for restrictions variables; in this case, + * the tested title will have the requested restriction. + * @covers AbuseFilter::generateTitleVars + * @dataProvider provideTitleVars + */ + public function testGenerateTitleVars( $prefix, $suffix, $options = null ) { + $varName = $prefix . $suffix; + list( $computed, $successfully ) = self::computeExpectedTitleVariable( $suffix, $options ); + if ( !$successfully ) { + if ( $computed === null ) { + $this->fail( "Given unknown title-related variable $varName." ); + } else { + $this->fail( "AbuseFilter variable $varName is computed wrongly." ); + } + } + + $variableHolder = AbuseFilter::generateTitleVars( self::$mTitle, $prefix ); + $actual = $variableHolder->getVar( $varName )->toNative(); + $this->assertSame( + $computed, + $actual, + "AbuseFilter variable $varName is computed wrongly." + ); + } + + /** + * Data provider for testGenerateUserVars + * @return array + */ + public function provideTitleVars() { + return [ + [ 'page', '_id' ], + [ 'page', '_namespace' ], + [ 'page', '_title' ], + [ 'page', '_prefixedtitle' ], + [ 'page', '_restrictions_create' ], + [ 'page', '_restrictions_create', 'restricted' ], + [ 'page', '_restrictions_edit' ], + [ 'page', '_restrictions_edit', 'restricted' ], + [ 'page', '_restrictions_move' ], + [ 'page', '_restrictions_move', 'restricted' ], + [ 'page', '_restrictions_upload' ], + [ 'page', '_restrictions_upload', 'restricted' ], + [ 'page', '_first_contributor' ], + [ 'page', '_recent_contributors' ], + [ 'moved_from', '_id' ], + [ 'moved_from', '_namespace' ], + [ 'moved_from', '_title' ], + [ 'moved_from', '_prefixedtitle' ], + [ 'moved_from', '_restrictions_create' ], + [ 'moved_from', '_restrictions_create', 'restricted' ], + [ 'moved_from', '_restrictions_edit' ], + [ 'moved_from', '_restrictions_edit', 'restricted' ], + [ 'moved_from', '_restrictions_move' ], + [ 'moved_from', '_restrictions_move', 'restricted' ], + [ 'moved_from', '_restrictions_upload' ], + [ 'moved_from', '_restrictions_upload', 'restricted' ], + [ 'moved_from', '_first_contributor' ], + [ 'moved_from', '_recent_contributors' ], + [ 'moved_to', '_id' ], + [ 'moved_to', '_namespace' ], + [ 'moved_to', '_title' ], + [ 'moved_to', '_prefixedtitle' ], + [ 'moved_to', '_restrictions_create' ], + [ 'moved_to', '_restrictions_create', 'restricted' ], + [ 'moved_to', '_restrictions_edit' ], + [ 'moved_to', '_restrictions_edit', 'restricted' ], + [ 'moved_to', '_restrictions_move' ], + [ 'moved_to', '_restrictions_move', 'restricted' ], + [ 'moved_to', '_restrictions_upload' ], + [ 'moved_to', '_restrictions_upload', 'restricted' ], + [ 'moved_to', '_first_contributor' ], + [ 'moved_to', '_recent_contributors' ], + ]; + } + + /** + * Check that _age variables are correct. They need a separate function to take into + * account the difference between timestamps due to test execution time + * + * @param string $prefix Prefix of the variable to test + * @covers AbuseFilter::generateTitleVars + * @dataProvider provideAgeVars + */ + public function testAgeVars( $prefix ) { + $varName = $prefix . '_age'; + + MWTimestamp::setFakeTime( 1514700000 ); + self::$mTitle = Title::newFromText( 'AbuseFilter test' ); + $page = WikiPage::factory( self::$mTitle ); + $page->doEditContent( + new WikitextContent( 'AbuseFilter _age variables test' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + + MWTimestamp::setFakeTime( 1514700123 ); + $variableHolder = AbuseFilter::generateTitleVars( self::$mTitle, $prefix ); + $actual = $variableHolder->getVar( $varName )->toNative(); + $this->assertEquals( + 123, + $actual, + "AbuseFilter variable $varName is computed wrongly. Expected: 123, actual: $actual." + ); + } + + /** + * Data provider for testAgeVars + * @return array + */ + public function provideAgeVars() { + return [ + [ 'page' ], + [ 'moved_from' ], + [ 'moved_to' ], + ]; + } + + /** + * Check that version comparing works well + * + * @param array $firstVersion [ stdClass, array ] + * @param array $secondVersion [ stdClass, array ] + * @param array $expected The differences + * @covers AbuseFilter::compareVersions + * @dataProvider provideVersions + */ + public function testCompareVersions( $firstVersion, $secondVersion, $expected ) { + $differences = AbuseFilter::compareVersions( $firstVersion, $secondVersion ); + + $this->assertSame( + $expected, + $differences, + 'AbuseFilter::compareVersions did not output the expected result.' + ); + } + + /** + * Data provider for testCompareVersions + * @return array + */ + public function provideVersions() { + return [ + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'OtherComments', + 'af_pattern' => '/*Other pattern*/', + 'af_comments' => 'Other comments', + 'af_deleted' => 1, + 'af_enabled' => 0, + 'af_hidden' => 1, + 'af_global' => 1, + 'af_group' => 'flow' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + 'af_public_comments', + 'af_pattern', + 'af_comments', + 'af_deleted', + 'af_enabled', + 'af_hidden', + 'af_global', + 'af_group', + ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'degroup' => [ + 'action' => 'degroup', + 'parameters' => [] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'OtherComments', + 'af_pattern' => '/*Other pattern*/', + 'af_comments' => 'Other comments', + 'af_deleted' => 1, + 'af_enabled' => 0, + 'af_hidden' => 1, + 'af_global' => 1, + 'af_group' => 'flow' + ], + [ + 'blockautopromote' => [ + 'action' => 'blockautopromote', + 'parameters' => [] + ] + ] + ], + [ + 'af_public_comments', + 'af_pattern', + 'af_comments', + 'af_deleted', + 'af_enabled', + 'af_hidden', + 'af_global', + 'af_group', + 'actions' + ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-my-best-warning' + ] + ], + 'degroup' => [ + 'action' => 'degroup', + 'parameters' => [] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Other Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 1, + 'af_global' => 0, + 'af_group' => 'flow' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-my-best-warning' + ] + ] + ] + ], + [ + 'af_pattern', + 'af_hidden', + 'af_group', + 'actions' + ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-beautiful-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'flow' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-my-best-warning' + ] + ] + ] + ], + [ + 'af_group', + 'actions' + ] + ], + ]; + } + + /** + * Check that row translating from abuse_filter_history to abuse_filter is working fine + * + * @param stdClass $row The row to translate + * @param array $expected The expected result + * @covers AbuseFilter::translateFromHistory + * @dataProvider provideHistoryRows + */ + public function testTranslateFromHistory( $row, $expected ) { + $actual = AbuseFilter::translateFromHistory( $row ); + + $this->assertEquals( + $expected, + $actual, + 'AbuseFilter::translateFromHistory produced a wrong output.' + ); + } + + /** + * Data provider for testTranslateFromHistory + * @return array + */ + public function provideHistoryRows() { + return [ + [ + (object)[ + 'afh_filter' => 1, + 'afh_user' => 0, + 'afh_user_text' => 'FilteredUser', + 'afh_timestamp' => '20180706142932', + 'afh_pattern' => '/*Pattern*/', + 'afh_comments' => 'Comments', + 'afh_flags' => 'enabled,hidden', + 'afh_public_comments' => 'Description', + 'afh_actions' => serialize( [ + 'degroup' => [], + 'disallow' => [] + ] ), + 'afh_deleted' => 0, + 'afh_changed_fields' => 'actions', + 'afh_group' => 'default' + ], + [ + (object)[ + 'af_pattern' => '/*Pattern*/', + 'af_user' => 0, + 'af_user_text' => 'FilteredUser', + 'af_timestamp' => '20180706142932', + 'af_comments' => 'Comments', + 'af_public_comments' => 'Description', + 'af_deleted' => 0, + 'af_id' => 1, + 'af_group' => 'default', + 'af_hidden' => 1, + 'af_enabled' => 1 + ], + [ + 'degroup' => [ + 'action' => 'degroup', + 'parameters' => [] + ], + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ] + ], + [ + (object)[ + 'afh_filter' => 5, + 'afh_user' => 0, + 'afh_user_text' => 'FilteredUser', + 'afh_timestamp' => '20180706145516', + 'afh_pattern' => '1 === 1', + 'afh_comments' => '', + 'afh_flags' => '', + 'afh_public_comments' => 'Our best filter', + 'afh_actions' => serialize( [ + 'warn' => [ + 'abusefilter-warning', + '' + ], + 'disallow' => [], + ] ), + 'afh_deleted' => 0, + 'afh_changed_fields' => 'af_pattern,af_comments,af_enabled,actions', + 'afh_group' => 'flow' + ], + [ + (object)[ + 'af_pattern' => '1 === 1', + 'af_user' => 0, + 'af_user_text' => 'FilteredUser', + 'af_timestamp' => '20180706145516', + 'af_comments' => '', + 'af_public_comments' => 'Our best filter', + 'af_deleted' => 0, + 'af_id' => 5, + 'af_group' => 'flow', + 'af_hidden' => 0, + 'af_enabled' => 0 + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning', + '' + ] + ], + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ] + ], + [ + (object)[ + 'afh_filter' => 7, + 'afh_user' => 1, + 'afh_user_text' => 'AnotherUser', + 'afh_timestamp' => '20160511185604', + 'afh_pattern' => 'added_lines irlike "lol" & summary == "ggwp"', + 'afh_comments' => 'Show vandals no mercy, for you shall receive none.', + 'afh_flags' => 'enabled,hidden', + 'afh_public_comments' => 'Whatever', + 'afh_actions' => serialize( [ + 'warn' => [ + 'abusefilter-warning', + '' + ], + 'disallow' => [], + 'block' => [ + 'blocktalk', + '8 hours', + 'infinity' + ] + ] ), + 'afh_deleted' => 0, + 'afh_changed_fields' => 'af_pattern,af_comments,af_enabled,af_public_comments,actions', + 'afh_group' => 'default' + ], + [ + (object)[ + 'af_pattern' => 'added_lines irlike "lol" & summary == "ggwp"', + 'af_user' => 1, + 'af_user_text' => 'AnotherUser', + 'af_timestamp' => '20160511185604', + 'af_comments' => 'Show vandals no mercy, for you shall receive none.', + 'af_public_comments' => 'Whatever', + 'af_deleted' => 0, + 'af_id' => 7, + 'af_group' => 'default', + 'af_hidden' => 1, + 'af_enabled' => 1 + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning', + '' + ] + ], + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ], + 'block' => [ + 'action' => 'block', + 'parameters' => [ + 'blocktalk', + '8 hours', + 'infinity' + ] + ] + ] + ] + ], + [ + (object)[ + 'afh_filter' => 131, + 'afh_user' => 15, + 'afh_user_text' => 'YetAnotherUser', + 'afh_timestamp' => '20180511185604', + 'afh_pattern' => 'user_name == "Thatguy"', + 'afh_comments' => '', + 'afh_flags' => 'hidden,deleted', + 'afh_public_comments' => 'No comment.', + 'afh_actions' => serialize( [ + 'throttle' => [ + '131', + '3,60', + 'user' + ], + 'tag' => [ + 'mytag', + 'yourtag' + ] + ] ), + 'afh_deleted' => 1, + 'afh_changed_fields' => 'af_pattern', + 'afh_group' => 'default' + ], + [ + (object)[ + 'af_pattern' => 'user_name == "Thatguy"', + 'af_user' => 15, + 'af_user_text' => 'YetAnotherUser', + 'af_timestamp' => '20180511185604', + 'af_comments' => '', + 'af_public_comments' => 'No comment.', + 'af_deleted' => 1, + 'af_id' => 131, + 'af_group' => 'default', + 'af_hidden' => 1, + 'af_enabled' => 0 + ], + [ + 'throttle' => [ + 'action' => 'throttle', + 'parameters' => [ + '131', + '3,60', + 'user' + ] + ], + 'tag' => [ + 'action' => 'tag', + 'parameters' => [ + 'mytag', + 'yourtag' + ] + ] + ] + ] + ] + ]; + } + + /** + * Given the name of a variable, naturally sets it to a determined amount + * + * @param string $old The old wikitext of the page + * @param string $new The new wikitext of the page + * @return array + */ + private static function computeExpectedEditVariable( $old, $new ) { + global $wgParser; + $popts = ParserOptions::newFromUser( self::$mUser ); + // Order matters here. Some variables rely on other ones. + $variables = [ + 'new_html', + 'new_pst', + 'new_text', + 'edit_diff', + 'edit_diff_pst', + 'new_size', + 'old_size', + 'edit_delta', + 'added_lines', + 'removed_lines', + 'added_lines_pst', + 'all_links', + 'old_links', + 'added_links', + 'removed_links' + ]; + + // Set required variables + self::$mVariables->setVar( 'old_wikitext', $old ); + self::$mVariables->setVar( 'new_wikitext', $new ); + self::$mVariables->setVar( 'summary', 'Testing page for AbuseFilter' ); + + $computedVariables = []; + foreach ( $variables as $var ) { + $success = true; + // Reset text variables since some operations are changing them. + $oldText = $old; + $newText = $new; + switch ( $var ) { + case 'edit_diff_pst': + $newText = self::$mVariables->getVar( 'new_pst' )->toString(); + // Intentional fall-through + case 'edit_diff': + $diffs = new Diff( explode( "\n", $oldText ), explode( "\n", $newText ) ); + $format = new UnifiedDiffFormatter(); + $result = $format->format( $diffs ); + break; + case 'new_size': + $result = strlen( $newText ); + break; + case 'old_size': + $result = strlen( $oldText ); + break; + case 'edit_delta': + $result = strlen( $newText ) - strlen( $oldText ); + break; + case 'added_lines_pst': + case 'added_lines': + case 'removed_lines': + $diffVariable = $var === 'added_lines_pst' ? 'edit_diff_pst' : 'edit_diff'; + $diff = self::$mVariables->getVar( $diffVariable )->toString(); + $line_prefix = $var === 'removed_lines' ? '-' : '+'; + $diff_lines = explode( "\n", $diff ); + $interest_lines = []; + foreach ( $diff_lines as $line ) { + if ( substr( $line, 0, 1 ) === $line_prefix ) { + $interest_lines[] = substr( $line, strlen( $line_prefix ) ); + } + } + $result = $interest_lines; + break; + case 'new_text': + $newHtml = self::$mVariables->getVar( 'new_html' )->toString(); + $result = StringUtils::delimiterReplace( '<', '>', '', $newHtml ); + break; + case 'new_pst': + case 'new_html': + $article = self::$mPage; + $content = ContentHandler::makeContent( $newText, $article->getTitle() ); + $editInfo = $article->prepareContentForEdit( $content ); + + if ( $var === 'new_pst' ) { + $result = $editInfo->pstContent->serialize( $editInfo->format ); + } else { + $result = $editInfo->output->getText(); + } + break; + case 'all_links': + $article = self::$mPage; + $content = ContentHandler::makeContent( $newText, $article->getTitle() ); + $editInfo = $article->prepareContentForEdit( $content ); + $result = array_keys( $editInfo->output->getExternalLinks() ); + break; + case 'old_links': + $article = self::$mPage; + $popts->setTidy( true ); + $edit = $wgParser->parse( $oldText, $article->getTitle(), $popts ); + $result = array_keys( $edit->getExternalLinks() ); + break; + case 'added_links': + case 'removed_links': + $oldLinks = self::$mVariables->getVar( 'old_links' )->toString(); + $newLinks = self::$mVariables->getVar( 'all_links' )->toString(); + $oldLinks = explode( "\n", $oldLinks ); + $newLinks = explode( "\n", $newLinks ); + + if ( $var === 'added_links' ) { + $result = array_diff( $newLinks, $oldLinks ); + } else { + $result = array_diff( $oldLinks, $newLinks ); + } + break; + default: + $success = false; + $result = null; + } + $computedVariables[$var] = [ $result, $success ]; + self::$mVariables->setVar( $var, $result ); + } + return $computedVariables; + } + + /** + * Check that the generated variables for edits are correct + * + * @param string $oldText The old wikitext of the page + * @param string $newText The new wikitext of the page + * @covers AbuseFilter::getEditVars + * @dataProvider provideEditVars + */ + public function testGetEditVars( $oldText, $newText ) { + global $wgLang; + self::$mTitle = Title::makeTitle( 0, 'AbuseFilter test' ); + self::$mPage = WikiPage::factory( self::$mTitle ); + + self::$mPage->doEditContent( + new WikitextContent( $oldText ), + 'Creating the test page', + EDIT_NEW, + false, + self::$mUser + ); + self::$mPage->doEditContent( + new WikitextContent( $newText ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + self::$mUser + ); + + $computeResult = self::computeExpectedEditVariable( $oldText, $newText ); + + $computedVariables = []; + foreach ( $computeResult as $varName => $computed ) { + if ( !$computed[1] ) { + $this->fail( "Given unknown edit variable $varName." ); + } + $computedVariables[$varName] = $computed[0]; + } + + self::$mVariables->addHolders( AbuseFilter::getEditVars( self::$mTitle, self::$mPage ) ); + + $actualVariables = []; + foreach ( self::$mVariables->mVars as $varName => $_ ) { + $actualVariables[$varName] = self::$mVariables->getVar( $varName )->toNative(); + } + + $differences = []; + foreach ( $computedVariables as $var => $computed ) { + if ( !isset( $actualVariables[$var] ) ) { + $this->fail( "AbuseFilter::getEditVars didn't set the $var variable." ); + } elseif ( $computed !== $actualVariables[$var] ) { + $differences[] = $var; + } + } + + $this->assertCount( + 0, + $differences, + 'The following AbuseFilter variables are computed wrongly: ' . $wgLang->commaList( $differences ) + ); + } + + /** + * Data provider for testGetEditVars + * @return array + */ + public function provideEditVars() { + return [ + [ + '[https://www.mediawiki.it/wiki/Extension:AbuseFilter AbuseFilter] test page', + 'Adding something to compute edit variables. Here are some diacritics to make sure ' . + "the test behaves well with unicode: Là giù cascherò io altresì.\n名探偵コナン.\n" . + "[[Help:Pre Save Transform|]] should make the difference as well.\n" . + 'Instead, [https://www.mediawiki.it this] is an external link.' + ], + [ + 'Adding something to compute edit variables. Here are some diacritics to make sure ' . + "the test behaves well with unicode: Là giù cascherò io altresì.\n名探偵コナン.\n" . + "[[Help:Pre Save Transform|]] should make the difference as well.\n" . + 'Instead, [https://www.mediawiki.it this] is an external link.', + '[https://www.mediawiki.it/wiki/Extension:AbuseFilter AbuseFilter] test page' + ], + [ + "A '''foo''' is not a ''bar''.", + "Actually, according to [http://en.wikipedia.org ''Wikipedia''], a '''''foo''''' " . + 'is <small>more or less</small> the same as a <b>bar</b>, except that a foo is ' . + 'usually provided together with a [[cellar door|]] to make it work<ref>Yes, really</ref>.' + ], + [ + 'This edit will be pretty smll', + 'This edit will be pretty small' + ] + ]; + } +} diff --git a/AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php b/AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php new file mode 100644 index 00000000..f573126f --- /dev/null +++ b/AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Tests for the AFPData class + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GPL-2.0-or-later + */ + +/** + * @group Test + * @group AbuseFilter + * + * @covers AbuseFilterTokenizer + * @covers AFPToken + * @covers AbuseFilterParser + * @covers AFPUserVisibleException + * @covers AFPException + */ +class AbuseFilterTokenizerTest extends MediaWikiTestCase { + /** + * @return AbuseFilterParser + */ + public static function getParser() { + static $parser = null; + if ( !$parser ) { + $parser = new AbuseFilterParser(); + } else { + $parser->resetState(); + } + return $parser; + } + + /** + * Base method for testing exceptions + * + * @param string $excep Identifier of the exception (e.g. 'unexpectedtoken') + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + */ + private function exceptionTest( $excep, $expr, $caller ) { + $parser = self::getParser(); + try { + $parser->parse( $expr ); + } catch ( AFPUserVisibleException $e ) { + $this->assertEquals( + $excep, + $e->mExceptionID, + "Exception $excep not thrown in AbuseFilterTokenizer::$caller" + ); + return; + } + + $this->fail( "Exception $excep not thrown in AbuseFilterTokenizer::$caller" ); + } + + /** + * Test the 'unclosedcomment' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterTokenizer::nextToken + * @dataProvider unclosedComment + */ + public function testUnclosedCommentException( $expr, $caller ) { + $this->exceptionTest( 'unclosedcomment', $expr, $caller ); + } + + /** + * Data provider for testUnclosedCommentException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unclosedComment() { + return [ + [ ' /**** / * /', 'nextToken' ], + ]; + } + + /** + * Test the 'unrecognisedtoken' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterTokenizer::nextToken + * @dataProvider unrecognisedToken + */ + public function testUnrecognisedTokenException( $expr, $caller ) { + $this->exceptionTest( 'unrecognisedtoken', $expr, $caller ); + } + + /** + * Data provider for testUnrecognisedTokenException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unrecognisedToken() { + return [ + [ '#', 'nextToken' ], + ]; + } + + /** + * Test the 'unclosedstring' exception + * + * @param string $expr The expression to test + * @param string $caller The function where the exception is thrown + * @covers AbuseFilterTokenizer::readStringLiteral + * @dataProvider unclosedString + */ + public function testUnclosedStringException( $expr, $caller ) { + $this->exceptionTest( 'unclosedstring', $expr, $caller ); + } + + /** + * Data provider for testUnclosedStringException + * The second parameter is the function where the exception is raised. + * One expression for each throw. + * + * @return array + */ + public function unclosedString() { + return [ + [ '"', 'readStringLiteral' ], + ]; + } +} diff --git a/AbuseFilter/tests/phpunit/parserTest.php b/AbuseFilter/tests/phpunit/parserTest.php deleted file mode 100644 index b253e858..00000000 --- a/AbuseFilter/tests/phpunit/parserTest.php +++ /dev/null @@ -1,145 +0,0 @@ -<?php -/** - * Tests for the AbuseFilter parser - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * - * @group Test - * @group AbuseFilter - * - * @licence GNU GPL v2+ - * @author Marius Hoch < hoo@online.de > - */ -class AbuseFilterParserTest extends MediaWikiTestCase { - /** - * @return AbuseFilterParser - */ - static function getParser() { - static $parser = null; - if ( !$parser ) { - $parser = new AbuseFilterParser(); - } - return $parser; - } - - /** - * @return [AbuseFilterParser] - */ - static function getParsers() { - static $parsers = null; - if ( !$parsers ) { - $parsers = [ - new AbuseFilterParser(), - new AbuseFilterCachingParser() - ]; - } - return $parsers; - } - - /** - * @dataProvider readTests - */ - public function testParser( $testName, $rule, $expected ) { - if ( !class_exists( 'AntiSpoof' ) && preg_match( '/(cc)?norm\(/i', $rule ) ) { - // The norm and ccnorm parser functions aren't working correctly without AntiSpoof - $this->markTestSkipped( 'Parser test ' . $testName . ' requires the AntiSpoof extension' ); - } - - foreach ( self::getParsers() as $parser ) { - $actual = $parser->parse( $rule ); - $this->assertEquals( $expected, $actual, 'Running parser test ' . $testName ); - } - } - - /** - * @return array - */ - public function readTests() { - $tests = []; - $testPath = __DIR__ . "/../parserTests"; - $testFiles = glob( $testPath . "/*.t" ); - - foreach ( $testFiles as $testFile ) { - $testName = substr( $testFile, 0, -2 ); - - $resultFile = $testName . '.r'; - $rule = trim( file_get_contents( $testFile ) ); - $result = trim( file_get_contents( $resultFile ) ) == 'MATCH'; - - $tests[] = [ - basename( $testName ), - $rule, - $result - ]; - } - - return $tests; - } - - /** - * Ensure that AbuseFilterTokenizer::OPERATOR_RE matches the contents - * and order of AbuseFilterTokenizer::$operators. - */ - public function testOperatorRe() { - $operatorRe = '/(' . implode( '|', array_map( function ( $op ) { - return preg_quote( $op, '/' ); - }, AbuseFilterTokenizer::$operators ) ) . ')/A'; - $this->assertEquals( $operatorRe, AbuseFilterTokenizer::OPERATOR_RE ); - } - - /** - * Ensure that AbuseFilterTokenizer::RADIX_RE matches the contents - * and order of AbuseFilterTokenizer::$bases. - */ - public function testRadixRe() { - $baseClass = implode( '', array_keys( AbuseFilterTokenizer::$bases ) ); - $radixRe = "/([0-9A-Fa-f]+(?:\.\d*)?|\.\d+)([$baseClass])?/Au"; - $this->assertEquals( $radixRe, AbuseFilterTokenizer::RADIX_RE ); - } - - /** - * Ensure the number of conditions counted for given expressions is right. - * - * @dataProvider condCountCases - */ - public function testCondCount( $rule, $expected ) { - $parser = self::getParser(); - // Set some variables for convenience writing test cases - $parser->setVars( array_combine( range( 'a', 'f' ), range( 'a', 'f' ) ) ); - $countBefore = AbuseFilter::$condCount; - $parser->parse( $rule ); - $countAfter = AbuseFilter::$condCount; - $actual = $countAfter - $countBefore; - $this->assertEquals( $expected, $actual, 'Condition count for ' . $rule ); - } - - /** - * @return array - */ - public function condCountCases() { - return [ - [ '(((a == b)))', 1 ], - [ 'contains_any(a, b, c)', 1 ], - [ 'a == b == c', 2 ], - [ 'a in b + c in d + e in f', 3 ], - [ 'true', 0 ], - [ 'a == a | c == d', 1 ], - [ 'a == b & c == d', 1 ], - ]; - } -} |