From: jenkins-bot Date: Sun, 7 Apr 2019 17:25:33 +0000 (+0000) Subject: Merge "Improve docs for Title::getInternalURL/getCanonicalURL" X-Git-Tag: 1.34.0-rc.0~2107 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=a38af7ba26579bb3004f673e44d39710887763aa;hp=febf5f7a155c7013f29eea6e14d8e0202ba4b91e Merge "Improve docs for Title::getInternalURL/getCanonicalURL" --- diff --git a/.eslintrc.json b/.eslintrc.json index 0c0a7b5d79..85d91b6662 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,6 @@ "rules": { "quote-props": [ "error", "as-needed" ], "max-len": "off", - "jquery/no-global-selector": "off" + "no-jquery/no-global-selector": "off" } } diff --git a/.fresnel.yml b/.fresnel.yml new file mode 100644 index 0000000000..2f71e4ba6d --- /dev/null +++ b/.fresnel.yml @@ -0,0 +1,43 @@ +warmup: true +runs: 5 +scenarios: + Load a page: + # The only page that exists by default is the main page. + # But its actual name is configurable/unknown (T216791). + # Omit 'title' to let MediaWiki show the default (which is the main page), + # and a query string to prevent a normalization redirect. + url: "{MW_SERVER}{MW_SCRIPT_PATH}/index.php?noredirectplz" + viewport: + width: 1100 + height: 700 + reports: + - navtiming + - paint + - transfer + probes: + - screenshot + - trace + Load the editor: + url: "{MW_SERVER}{MW_SCRIPT_PATH}/index.php?action=edit" + viewport: + width: 1100 + height: 700 + reports: + - navtiming + - paint + - transfer + probes: + - screenshot + - trace + View recent changes: + url: "{MW_SERVER}{MW_SCRIPT_PATH}/index.php?title=Special:RecentChanges" + viewport: + width: 1100 + height: 700 + reports: + - navtiming + - paint + - transfer + probes: + - screenshot + - trace diff --git a/.gitignore b/.gitignore index 2f17bc6d5f..8cacb1ee30 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ sftp-config.json # MediaWiki install & usage /cache +/docs/coverage /docs/js /images/[0-9a-f] /images/archive @@ -49,8 +50,10 @@ sftp-config.json # Building & testing npm-debug.log node_modules/ +/resources/lib/.foreign /tests/phpunit/phpunit.phar /tests/selenium/log +.eslintcache # Composer /vendor diff --git a/.phan/config.php b/.phan/config.php new file mode 100644 index 0000000000..12e723d469 --- /dev/null +++ b/.phan/config.php @@ -0,0 +1,143 @@ + '.phan/internal_stubs/memcached.phan_php', + 'oci8' => '.phan/internal_stubs/oci8.phan_php', + 'sqlsrv' => '.phan/internal_stubs/sqlsrv.phan_php', + 'tideways' => '.phan/internal_stubs/tideways.phan_php', +]; + +$cfg['directory_list'] = [ + 'includes/', + 'languages/', + 'maintenance/', + 'mw-config/', + 'resources/', + 'vendor/', + '.phan/stubs/', +]; + +$cfg['exclude_analysis_directory_list'] = [ + 'vendor/', + '.phan/stubs/', + // The referenced classes are not available in vendor, only when + // included from composer. + 'includes/composer/', + // Directly references classes that only exist in Translate extension + 'maintenance/language/', + // External class + 'includes/libs/jsminplus.php', +]; + +$cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [ + // approximate error count: 18 + "PhanAccessMethodInternal", + // approximate error count: 17 + "PhanCommentParamOnEmptyParamList", + // approximate error count: 29 + "PhanCommentParamWithoutRealParam", + // approximate error count: 2 + "PhanCompatibleNegativeStringOffset", + // approximate error count: 21 + "PhanParamReqAfterOpt", + // approximate error count: 26 + "PhanParamSignatureMismatch", + // approximate error count: 4 + "PhanParamSignatureMismatchInternal", + // approximate error count: 127 + "PhanParamTooMany", + // approximate error count: 2 + "PhanTraitParentReference", + // approximate error count: 30 + "PhanTypeArraySuspicious", + // approximate error count: 27 + "PhanTypeArraySuspiciousNullable", + // approximate error count: 26 + "PhanTypeComparisonFromArray", + // approximate error count: 63 + "PhanTypeInvalidDimOffset", + // approximate error count: 7 + "PhanTypeInvalidLeftOperandOfIntegerOp", + // approximate error count: 2 + "PhanTypeInvalidRightOperandOfIntegerOp", + // approximate error count: 154 + "PhanTypeMismatchArgument", + // approximate error count: 27 + "PhanTypeMismatchArgumentInternal", + // approximate error count: 2 + "PhanTypeMismatchDimEmpty", + // approximate error count: 27 + "PhanTypeMismatchDimFetch", + // approximate error count: 10 + "PhanTypeMismatchForeach", + // approximate error count: 77 + "PhanTypeMismatchProperty", + // approximate error count: 84 + "PhanTypeMismatchReturn", + // approximate error count: 12 + "PhanTypeObjectUnsetDeclaredProperty", + // approximate error count: 9 + "PhanTypeSuspiciousNonTraversableForeach", + // approximate error count: 3 + "PhanTypeSuspiciousStringExpression", + // approximate error count: 22 + "PhanUndeclaredConstant", + // approximate error count: 3 + "PhanUndeclaredInvokeInCallable", + // approximate error count: 237 + "PhanUndeclaredMethod", + // approximate error count: 846 + "PhanUndeclaredProperty", + // approximate error count: 2 + "PhanUndeclaredVariableAssignOp", + // approximate error count: 55 + "PhanUndeclaredVariableDim", +] ); + +$cfg['ignore_undeclared_variables_in_global_scope'] = true; +$cfg['globals_type_map']['IP'] = 'string'; + +return $cfg; diff --git a/.phan/internal_stubs/memcached.phan_php b/.phan/internal_stubs/memcached.phan_php new file mode 100644 index 0000000000..8a85bafe39 --- /dev/null +++ b/.phan/internal_stubs/memcached.phan_php @@ -0,0 +1,180 @@ + - + + + - @@ -23,17 +24,8 @@ - + @@ -78,16 +70,9 @@ Whitelist existing violations, but enable the sniff to prevent any new occurrences. --> - */includes/media/XCF\.php */includes/Feed\.php - */includes/libs/xmp/XMP\.php - */includes/jobqueue/JobSpecification\.php - */includes/RevisionList\.php */includes/installer/PhpBugTests\.php - */includes/exception/LocalizedException\.php */includes/specials/SpecialMostinterwikis\.php - */includes/cache/CacheDependency\.php - */includes/cache/CacheHelper\.php */includes/compat/XMPReader\.php */includes/diff/DairikiDiff\.php */includes/specials/SpecialAncientpages\.php @@ -113,7 +98,6 @@ */includes/specials/SpecialMostlinkedtemplates\.php */includes/specials/SpecialMostrevisions\.php */includes/specials/SpecialMovepage\.php - */includes/specials/SpecialMyRedirectPages\.php */includes/specials/SpecialNewimages\.php */includes/specials/SpecialRandompage\.php */includes/specials/SpecialShortpages\.php @@ -138,7 +122,6 @@ */maintenance/benchmarks/bench_Wikimedia_base_convert\.php */maintenance/benchmarks/bench_delete_truncate\.php */maintenance/benchmarks/bench_if_switch\.php - */maintenance/benchmarks/bench_strtr_str_replace\.php */maintenance/benchmarks/bench_utf8_title_check\.php */maintenance/benchmarks/bench_wfIsWindows\.php */maintenance/cleanupTable.inc @@ -176,8 +159,12 @@ */profileinfo\.php */languages/*\.php - - */tests/*\.php + + */tests/parser/*\.php + */tests/phan/*\.php + */tests/phpunit/maintenance/*\.php + */tests/phpunit/bootstrap\.php + */tests/phpunit/phpunit\.php - */tests/*\.php + + */tests/phpunit/includes/GlobalFunctions/*\.php + */tests/phpunit/maintenance/*\.php @@ -217,56 +205,32 @@ Whitelist existing violations, but enable the sniff to prevent any new occurrences. --> - */includes/actions/HistoryAction\.php */includes/api/ApiErrorFormatter\.php */includes/api/ApiImport\.php */includes/api/ApiMessage\.php */includes/api/ApiOpenSearch\.php */includes/api/ApiRsd\.php - */includes/api/ApiUsageException\.php - */includes/AuthPlugin\.php - */includes/cache/CacheDependency\.php - */includes/cache/CacheHelper\.php */includes/compat/XMPReader\.php - */includes/deferred/CdnCacheUpdate\.php */includes/diff/DairikiDiff\.php - */includes/diff/DiffEngine\.php - */includes/exception/LocalizedException\.php */includes/Feed\.php */includes/filerepo/file/LocalFile\.php - */includes/gallery/PackedOverlayImageGallery\.php - */includes/HistoryBlob\.php */includes/htmlform/HTMLFormElement\.php - */includes/jobqueue/aggregator/JobQueueAggregator\.php - */includes/jobqueue/JobQueue\.php - */includes/jobqueue/JobSpecification\.php */includes/libs/filebackend/FileBackendStore\.php */includes/libs/filebackend/FSFileBackend\.php */includes/libs/filebackend/SwiftFileBackend\.php */includes/logging/LogEntry\.php */includes/logging/LogFormatter\.php - */includes/media/MediaTransformOutput\.php */includes/media/SVGMetadataExtractor\.php */includes/parser/Preprocessor_DOM\.php */includes/parser/Preprocessor_Hash\.php */includes/parser/Preprocessor\.php */includes/PathRouter\.php - */includes/poolcounter/PoolCounter\.php - */includes/PrefixSearch\.php */includes/profiler/SectionProfiler\.php - */includes/RevisionList\.php - */includes/search/SearchEngine\.php */includes/specialpage/LoginSignupSpecialPage\.php - */includes/specialpage/RedirectSpecialPage\.php */includes/specials/forms/PreferencesFormLegacy\.php - */includes/specials/SpecialListusers\.php - */includes/specials/SpecialMyRedirectPages\.php - */includes/specials/SpecialUploadStash\.php */includes/StubObject\.php - */includes/upload/UploadFromChunks\.php */includes/upload/UploadStash\.php */includes/utils/AutoloadGenerator\.php - */includes/WebResponse\.php */maintenance/dumpIterator\.php */maintenance/Maintenance\.php */maintenance/findDeprecated\.php @@ -296,14 +260,6 @@ */includes/libs/filebackend/FSFileBackend\.php */includes/shell/Command\.php */includes/shell/Shell\.php - */tests/phpunit/structure/StructureTest\.php - - - - */tests/phpunit/structure/StructureTest\.php +* Fix syntax errors introduced in 1.23.16 when running PHP 5.3. + == MediaWiki 1.23.16 == This is a security and maintenance release of the MediaWiki 1.23 branch. @@ -4793,21 +4916,21 @@ This is a security and maintenance release of the MediaWiki 1.23 branch. * Submitting the lgtoken and lgpassword parameters in the query string to action=login is now deprecated and outputs a warning. They should be submitted in the POST body instead. -* (T109140) (T122209) SECURITY: Special:UserLogin and Special:Search allow redirect - to interwiki links. +* (T109140) (T122209) SECURITY: Special:UserLogin and Special:Search allow + redirect to interwiki links. * (T144845) SECURITY: XSS in SearchHighlighter::highlightText() when $wgAdvancedSearchHighlighting is true. * (T125177) SECURITY: API parameters may now be marked as "sensitive" to keep their values out of the logs. -* (T150044) SECURITY: "Mark all pages visited" on the watchlist now requires a CSRF - token. +* (T150044) SECURITY: "Mark all pages visited" on the watchlist now requires a + CSRF token. * (T156184) SECURITY: Escape content model/format url parameter in message. * (T151735) SECURITY: SVG filter evasion using default attribute values in DTD declaration. -* (T48143) SECURITY: Spam blacklist ineffective on encoded URLs inside file inclusion - syntax's link parameter. -* (T108138) SECURITY: Sysops can undelete pages, although the page is protected against - it. +* (T48143) SECURITY: Spam blacklist ineffective on encoded URLs inside file + inclusion syntax's link parameter. +* (T108138) SECURITY: Sysops can undelete pages, although the page is protected + against it. == MediaWiki 1.23.15 == @@ -4826,7 +4949,8 @@ This is a maintenance release of the MediaWiki 1.23 branch. * (T129738) SECURITY: Make $wgBlockDisablesLogin also restrict logged in permissions * (T129738) SECURITY: Make blocks log users out if $wgBlockDisablesLogin is true -* (T115333) SECURITY: Check read permission when loading page content in ApiParse +* (T115333) SECURITY: Check read permission when loading page content in + ApiParse * Remove support for $wgWellFormedXml = false, all output is now well formed == MediaWiki 1.23.13 == @@ -5018,7 +5142,8 @@ This is a security and maintenance release of the MediaWiki 1.23 branch. * (bug 65839) SECURITY: Prevent external resources in SVG files. * (bug 67025) Special:Watchlist: Don't try to render empty row. -* (bug 66922) Don't allow some E_NOTICE messages to end up in the LocalSettings.php. +* (bug 66922) Don't allow some E_NOTICE messages to end up in the + LocalSettings.php. * (bug 66467) FileBackend: Avoid using popen() when "parallelize" is disabled. * (bug 66428) MimeMagic: Don't seek before BOF. This has weird side effects like only extracting the tail of the file partially or not at all. @@ -5159,7 +5284,8 @@ This is a security and maintenance release of the MediaWiki 1.23 branch. buttons. * Special:UserLogin/signup now does AJAX checks for invalid and taken usernames, displaying the error live. -* Added BaseTemplateAfterPortlet hook to allow injecting html after portlets in skins. +* Added BaseTemplateAfterPortlet hook to allow injecting html after portlets in + skins. * Support has been added for a JSON based localisation file format. The installer has been updated to use it. * Changes to content typography (colors, line-height etc.). See @@ -5168,11 +5294,12 @@ This is a security and maintenance release of the MediaWiki 1.23 branch. single icon (from nine). This should not affect local rules unless they were re-using these icons, which have now been deleted. * ResourceLoader: mw.loader.using() now implements a Promise interface. -* Add new hook ChangesListInitRows accessed via ChangesList::initChangesListRows. +* Add new hook ChangesListInitRows accessed via + ChangesList::initChangesListRows. If called by the ChangesList consumer this gives extensions a chance to batch process the result set prior to rendering. -* A PoolCounterRedis class was added which can be make use of in $wgPoolCounterConf. - This requires at least one Redis 2.6+ server. +* A PoolCounterRedis class was added which can be make use of in + $wgPoolCounterConf. This requires at least one Redis 2.6+ server. * $wgProfileToDatabase was removed. Set $wgProfiler to ProfilerSimpleDB in StartProfiler.php instead of using this. * (bug 63444) Made it possible to change the indent string (default: 4 spaces) @@ -5544,28 +5671,43 @@ This is a maintenance release of the MediaWiki 1.22 branch. This is a security release of the MediaWiki 1.22 branch. === Changes since 1.22.11 === -* (bug 70672) SECURITY: OutputPage: Remove separation of css and js module allowance. +* (bug 70672) SECURITY: OutputPage: Remove separation of css and js module + allowance. == MediaWiki 1.22.11 == This is a security release of the MediaWiki 1.22 branch. === Changes since 1.22.10 === -* (bug 69008) SECURITY: Enhance CSS filtering in SVG files. Filter Style[edit] - !! html/parsoid

Style

@@ -25550,7 +25154,6 @@ __TOC__

Script[edit]

- !! html/parsoid

Script

@@ -25569,7 +25172,6 @@ __TOC__

x[edit]

- !! html/parsoid

x

@@ -25589,7 +25191,6 @@ title=[[Main Page]] {{int:T34057}} !! html

Headline text[edit]

- !! end !! test @@ -25708,7 +25309,6 @@ nowiki inside link inside heading (T20295) ==[[foo|xyz]]== !! html

xyz[edit]

- !! end !! test @@ -25717,8 +25317,7 @@ new support for bdi element (T33817)

ולדימיר לנין (ברוסית: Владимир Ленин, 24 באפריל 1870–22 בינואר 1924) הוא מנהיג פוליטי קומוניסטי רוסי.

!! html

ולדימיר לנין (ברוסית: Владимир Ленин, 24 באפריל 1870–22 בינואר 1924) הוא מנהיג פוליטי קומוניסטי רוסי.

- -!!end +!! end !! test Ignore pipe between table row attributes @@ -25736,7 +25335,6 @@ Ignore pipe between table row attributes bar - !! end !!test @@ -25865,7 +25463,6 @@ Lead

Section 3[edit]

Section 4[edit]

Section 5[edit]

- !! end @@ -25950,7 +25547,7 @@ parsoid=wt2html,wt2wt !! html/parsoid

foo

caption

bar

-
+
!! end !! test @@ -25960,8 +25557,7 @@ parsoid=wt2html,wt2wt !! wikitext '''foo[[File:Foobar.jpg|thumb|caption]]bar''' !! html/php+tidy -

foo

caption

bar -

+

foo

caption

bar

!! html/parsoid

foo

caption

bar

!! end @@ -25975,7 +25571,7 @@ parsoid=wt2html,wt2wt !! html/php+tidy
Foobar.jpg
!! html/parsoid -
+
!! end #### ---------------------------------------------------------------- @@ -26131,12 +25727,12 @@ parsoid=html2wt

=foo=

=foo=

-

=foo=

-

=foo=

-

=foo=

-

=foo=

-
=foo=
-
=foo=
+

=foo=

+

=foo=

+

=foo=

+

=foo=

+
=foo=
+
=foo=
!! wikitext = =foo= = @@ -26150,8 +25746,7 @@ parsoid=html2wt =====foo===== ======foo====== =======foo======= - -!!end +!! end !! test Headings: 2. Outside heading nest on a single line

foo

*bar @@ -26262,7 +25857,6 @@ parsoid=wt2html,html2html

=[edit]

==[edit]

=[edit]

- !! html/parsoid

= ==

@@ -26394,7 +25988,6 @@ new ==A== a - !! end !! test @@ -26407,7 +26000,6 @@ parsoid=wt2html =============== !! html/php
===[edit]
- !! html/parsoid
===
!! end @@ -26751,7 +26343,6 @@ parsoid=html2wt a
b||c
- !! end !! test @@ -26763,7 +26354,6 @@ parsoid=html2wt foo!!bar - !! wikitext {| |foo!!bar @@ -26779,7 +26369,6 @@ parsoid=html2wt foo!bar - !! wikitext {| !foo!bar @@ -26815,7 +26404,6 @@ parsoid=html2wt foo!!bar - !! end !! test @@ -26839,7 +26427,6 @@ parsoid=html2wt foo||bar - !! end !! test @@ -26854,7 +26441,6 @@ parsoid=html2wt -bar - !! wikitext {| !-bar @@ -26875,7 +26461,6 @@ parsoid=html2wt +bar - !! wikitext {| !+bar @@ -26938,7 +26523,6 @@ bar|baz x
a|b
- !! end !! test @@ -26966,7 +26550,6 @@ parsoid=html2wt -2 - !! end !! test @@ -26993,7 +26576,6 @@ parsoid=html2wt x } - !! end !! test @@ -27088,7 +26670,6 @@ parsoid=html2wt bar> - !! end #### --------------- Links ---------------- @@ -27137,7 +26718,7 @@ parsoid=html2wt [[Foo|x [http://google.com g] x]] [[Foo|[[Bar]]]] [[Foo|x [[Bar]] x]] -[[Foo||Bar]] +[[Foo||Bar]] [[Foo|]]bar]] [[Foo|[[bar]] [[Foo|x [[ y]] @@ -27973,8 +27554,7 @@ parsoid=wt2html,html2html !! wikitext

Foo -

+

Foo

!! html/parsoid

Foo

!! end @@ -27992,13 +27572,11 @@ parsoid=wt2html,html2html Foo - !! html/parsoid
Foo
- !! end !! test @@ -28017,14 +27595,12 @@ parsoid=wt2html,html2html Bar - !! html/parsoid
Foo Bar
- !! end !!test @@ -28039,8 +27615,7 @@ Accept empty td cell attribute foo - -!!end +!! end !!test Non-empty attributes in th-cells @@ -28054,8 +27629,7 @@ Non-empty attributes in th-cells Foo Bar - -!!end +!! end !!test Accept empty attributes in th-cells @@ -28069,8 +27643,7 @@ Accept empty attributes in th-cells foo bar - -!!end +!! end !!test Empty table rows go away @@ -28090,7 +27663,6 @@ Empty table rows go away - !! end ### @@ -28109,7 +27681,6 @@ RT-ed inter-element separators should be valid separators
- !! html/parsoid @@ -28157,8 +27728,7 @@ Empty TD followed by TD with tpl-generated attribute
foo
- -!!end +!! end !!test Indented table with an empty td @@ -28176,8 +27746,7 @@ Indented table with an empty td foo - -!!end +!! end !! test Indented table with blank lines in between (T85627) @@ -28194,7 +27763,6 @@ Indented table with blank lines in between (T85627)


- !! html/parsoid
foo @@ -28216,7 +27784,6 @@ Indented block & table
foo
- !! html/parsoid
foo
@@ -28237,7 +27804,6 @@ Indent and comment before table row
there
- !! html/parsoid @@ -28291,8 +27857,7 @@ Empty TR followed by mixed-ws-comment line should RT correctly
- -!!end +!! end !!test Multi-line image caption generated by templates with/without trailing newlines @@ -28557,8 +28122,7 @@ parsoid=wt2wt,wt2html !! wikitext {{echo|hi
hello}} !! html/php+tidy -hi

hello -

+hi

hello

!! html/parsoid

hi

hello

!! end @@ -28584,6 +28148,7 @@ parsoid=wt2html !! end # Parsoid only for T66747 +# (Also core doesn't define {{#if}} in default install) !! test Properly encapsulate empty-content transclusions in fosterable positions !! wikitext @@ -28610,7 +28175,6 @@ hello {{OpenTable}} |} !! html/parsoid - !! end !! test @@ -28922,6 +28486,7 @@ parsoid={ !! test Image: Modifying alt attribute of an image (T58400) !! options +disabled parsoid={ "modes": ["wt2wt"], "changes": [ @@ -28979,7 +28544,7 @@ Image: Block level image should have \n before and after 456 !! html/parsoid

123

-
+

456

!! end @@ -28991,7 +28556,7 @@ Image: New block level image should have \n before and after (existing content) 456 !! html/parsoid

123

-
+

456

!! end @@ -29073,7 +28638,7 @@ Image: Invalid title as link

link=<

!! html/parsoid -

+

!! end !! test @@ -29090,11 +28655,11 @@ Various link types in alt and link options

wikipedia:Foo

!! html/parsoid -

Main Page

+

Main Page

-

Media:Thumb.png

+

Media:Thumb.png

-

wikipedia:Foo

+

wikipedia:Foo

!! end !! test @@ -30424,6 +29989,28 @@ parsoid= { |} !! end +## Don't necessarily expect this to roundtrip, but run serialization to catch crashers +!! test +File in link scenarios +!! options +parsoid={ + "modes": ["wt2html","html2wt"], + "suppressErrors": true +} +!! wikitext +[http://www.google.com [[File:Foobar.jpg|123]]] + +[http://www.google.com [[File:Foobar.jpg|thumb|123]]] +!! html/php+tidy +

123 +

+
123
+!! html/parsoid +

+ +
123
+!! end + # -------------------------------------------- # Tests spec'ing wikitext serialization norms | # -------------------------------------------- @@ -30754,7 +30341,6 @@ parsoid={ [[foo]] x - !! end !! test @@ -31565,7 +31151,7 @@ Thumbnail output !! html/php+tidy
Thumb.png
!! html/parsoid -
+
!! end !! test @@ -31758,7 +31344,6 @@ Decoding of HTML entities in embedded HTML tags
x
!! html/php
x
- !! html/parsoid
x
!! end @@ -32014,6 +31599,21 @@ parsoid={ [[stats:v2/#/fr.wikipedia.org/reading/page-views-by-country/normal%7Cmap%7C2-Year~2016060100~2018071100%7C~total|10]] !! end +## FIXME: "gerrit" isn't in PHP's setupInterwikis +!! test +T199926: Hash only interwiki link +!! wikitext +[[meatball:Test#1/2]] +[[gerrit:#/q/project:mediawiki/services/parsoid|Gerrit]] +!! html/php+tidy +

meatball:Test#1/2 +Gerrit +

+!! html/parsoid +

meatball:Test#1/2 +Gerrit

+!! end + !! test T179544: {{anchorencode:}} output should be always usable in links !! config @@ -32033,6 +31633,7 @@ wgFragmentMode=[ 'html5' ] !! test Section wrapping for well-nested sections (no leading content) !! options +notoc parsoid={ "wrapSections": true } @@ -32054,23 +31655,43 @@ e =3= f +!! html/php+tidy + +

1[edit]

+

a +

+

2[edit]

+

b +

+

2.1[edit]

+

c +

+

2.2[edit]

+

d +

+

2.2.1[edit]

+

e +

+

3[edit]

+

f +

!! html/parsoid -

1

+

1

a

-

2

+

2

b

-

2.1

+

2.1

c

-

2.2

+

2.2

d

-

2.2.1

+

2.2.1

e

-

3

+

3

f

@@ -32079,6 +31700,7 @@ f !! test Section wrapping for well-nested sections (with leading content) !! options +notoc parsoid={ "wrapSections": true } @@ -32097,6 +31719,21 @@ b ==2.1== c +!! html/php+tidy +

Para 1. +

+Para 2 with a

nested in it
+

Para 3. +

+

1[edit]

+

a +

+

2[edit]

+

b +

+

2.1[edit]

+

c +

!! html/parsoid

Para 1.

@@ -32104,13 +31741,13 @@ c

Para 3.

-

1

+

1

a

-

2

+

2

b

-

2.1

+

2.1

c

@@ -32119,6 +31756,7 @@ c !! test Section wrapping with template-generated sections (good nesting 1) !! options +notoc parsoid={ "wrapSections": true } @@ -32130,22 +31768,35 @@ a ==1.1== b }} - ==1.2== c =2= d +!! html/php+tidy + +

1[edit]

+

a +

+

1.1

+

b +

+

1.2[edit]

+

c +

+

2[edit]

+

d +

!! html/parsoid -

1

+

1

a

1.1

b

-

1.2

+

1.2

c

-

2

+

2

d

!! end @@ -32154,6 +31805,7 @@ d !! test Section wrapping with template-generated sections (good nesting 2) !! options +notoc parsoid={ "wrapSections": true, "modes": ["wt2html", "wt2wt"] @@ -32170,6 +31822,20 @@ d }} =2= e +!! html/php+tidy + +

1[edit]

+

a +

+

1.1

+

b +

+

1.1.1

+

d +

+

2[edit]

+

e +

!! html/parsoid

1

a

@@ -32187,6 +31853,7 @@ e !! test Section wrapping with template-generated sections (good nesting 3) !! options +notoc parsoid={ "wrapSections": true, "modes": ["wt2html", "wt2wt"] @@ -32206,6 +31873,24 @@ d }} =2= e +!! html/php+tidy + +

1[edit]

+

a +

x +

+

1.1

+

b +

+

1.2

+

c +

+

1.2.1

+

d +

+

2[edit]

+

e +

!! html/parsoid

1

a

@@ -32228,6 +31913,7 @@ e !! test Section wrapping with template-generated sections (bad nesting 1) !! options +notoc parsoid={ "wrapSections": true } @@ -32235,24 +31921,31 @@ parsoid={
a -{{echo| +{{echo|1= =1= b }} c
+!! html/php+tidy +
+

a +

+

1

+

b +

c +

+
!! html/parsoid

a

- -

1

-

b -

+

1

+

b

-

c

-
+

c

+
!! end # Because of section-wrapping and template-wrapping interactions, @@ -32262,6 +31955,7 @@ c !! test Section wrapping with template-generated sections (bad nesting 2) !! options +notoc parsoid={ "wrapSections": true } @@ -32280,8 +31974,23 @@ d =3= e +!! html/php+tidy + +

1[edit]

+

a +

+

2

+

b +

+

2.1

+

c +

d +

+

3[edit]

+

e +

!! html/parsoid -

1

+

1

a

2

@@ -32291,7 +32000,7 @@ e

d

-

3

+

3

e

!! end @@ -32304,6 +32013,7 @@ e !! test Section wrapping with template-generated sections (bad nesting 3) !! options +notoc parsoid={ "wrapSections": true, "modes": ["wt2html", "wt2wt"] @@ -32323,6 +32033,21 @@ d =3= e +!! html/php+tidy + +

1[edit]

+

a +

+

1.2

+

b +

+

2

+

c +

d +

+

3[edit]

+

e +

!! html/parsoid

1

a

@@ -32340,6 +32065,7 @@ e !! test Section wrapping with uneditable lead section + div wrapping multiple sections !! options +notoc parsoid={ "wrapSections": true } @@ -32362,24 +32088,45 @@ d ==3.1== e +!! html/php+tidy +

foo +

+
+ +

1[edit]

+

a +

+

1.1[edit]

+

b +

+

2[edit]

+

c +

+
+

3[edit]

+

d +

+

3.1[edit]

+

e +

!! html/parsoid

foo

-

1

+

1

a

-

1.1

+

1.1

b

-

2

+

2

c

-

3

+

3

d

-

3.1

+

3.1

e

!! end @@ -32387,6 +32134,7 @@ e !! test Section wrapping with editable lead section + div overlapping multiple sections !! options +notoc parsoid={ "wrapSections": true } @@ -32411,26 +32159,51 @@ f ==3.1== g +!! html/php+tidy +

foo +

+ +

1[edit]

+

a +

+
+

b +

+

1.1[edit]

+

c +

+

2[edit]

+

d +

+
+

e +

+

3[edit]

+

f +

+

3.1[edit]

+

g +

!! html/parsoid

foo

-

1

+

1

a

b

-

1.1

+

1.1

c

-

2

+

2

d

e

-

3

+

3

f

-

3.1

+

3.1

g

!! end @@ -32438,6 +32211,7 @@ g !! test HTML header tags should not be wrapped in section tags !! options +notoc parsoid={ "wrapSections": true } @@ -32451,21 +32225,30 @@ foo

c

=d= +!! html/php+tidy +

foo +

+ +

a

+

b[edit]

+

c

+

d[edit]

!! html/parsoid

foo

a

-

b

+

b

c

-

d

+

d

!! end !! test Lead section containing only whitespace and comments. !! options +notoc parsoid={ "wrapSections": true } @@ -32477,19 +32260,28 @@ a =2= b +!! html/php+tidy +

1[edit]

+

a +

+

2[edit]

+

b +

!! html/parsoid
-

1

+

1

a

-

2

-

b

+

2

+

b

+
!! end !! test -Pseudo-sections emitted by templates should have id -2 +Pseudo-sections emitted by templates should have id -2 !! options +notoc parsoid={ "wrapSections": true } @@ -32500,6 +32292,13 @@ foo ==b== }} +!! html/php+tidy +

foo +

+
+

a

+

b

+
!! html/parsoid

foo

@@ -32509,6 +32308,118 @@ foo
!! end +!! test +T213468: Transcluded sections don't get PHP section numbers +!! options +notoc +parsoid={ + "wrapSections": true +} +!! wikitext +==PHP section=1== +{{echo|1= +== This is counted as if it were section 2 == +}} +==PHP section=3== +!! html/php+tidy +

PHP section=1[edit]

+

This is counted as if it were section 2

+

PHP section=3[edit]

+!! html/parsoid +

PHP section=1

+

This is counted as if it were section 2

+

PHP section=3

+!! end + +!! test +T213468: Corner cases in edit section ID assignment in tokenizer +!! options +notoc +parsoid={ + "wrapSections": true +} +!! wikitext +==PHP section=1== +{{echo|Not a section| +== This is counted as if it were section 2 == +}} +==PHP section=3== +{{echo3|1= +== This is counted as if it were section 4 == +}} +==PHP section=5== +{{#tag:p|Not a section|data-ignored= +== This is counted as if it were section 6 == +}} +==PHP section=7== +{{echo|1=Not a ==heading==}} +==PHP section=8== +[[File:Foobar.jpg|thumb| +==This is section 9, even though it's in a caption== +]] +==PHP section=10== +!! html/php+tidy + +

PHP section=1[edit]

+

Not a section +

+

PHP section=3[edit]

+

This is counted as if it were section 4

+

This is counted as if it were section 4

+

This is counted as if it were section 4

+

PHP section=5[edit]

+

Not a section

+

PHP section=7[edit]

+

Not a ==heading== +

+

PHP section=8[edit]

+

This is section 9, even though it's in a caption[edit]

+

PHP section=10[edit]

+!! html/parsoid +

PHP section=1

+

Not a section

+

PHP section=3

+

This is counted as if it were section 4

+

This is counted as if it were section 4

+

This is counted as if it were section 4

+

PHP section=5

+

Not a section

+

PHP section=7

+

Not a ==heading==

+

PHP section=8

+
+

This is section 9, even though it's in a caption

+
+

PHP section=10

+!! end + +!! test +T215628: Section numbering and and on a page +!! options +notoc +parsoid={ + "wrapSections": true +} +!! wikitext +==PHP section=1== + +==PHP section=2== + +==PHP section=3== + +==This is not counted as section 4== + +==PHP section=4== +!! html/php+tidy + +

PHP section=1[edit]

+

PHP section=2[edit]

+

PHP section=3[edit]

+

PHP section=4[edit]

+!! html/parsoid +PARSOID HAS A BUG HERE: T215628 +!! end + ########################################################################## Tests demonstrating white-space insensitivity in input wikitext for wikitext headings, wikitext list items, and wikitext table captions, diff --git a/tests/phan/config.php b/tests/phan/config.php deleted file mode 100644 index fa351ea123..0000000000 --- a/tests/phan/config.php +++ /dev/null @@ -1,468 +0,0 @@ - array_merge( - function_exists( 'register_postsend_function' ) ? [] : [ 'tests/phan/stubs/hhvm.php' ], - function_exists( 'wikidiff2_do_diff' ) ? [] : [ 'tests/phan/stubs/wikidiff.php' ], - function_exists( 'tideways_enable' ) ? [] : [ 'tests/phan/stubs/tideways.php' ], - class_exists( PEAR::class ) ? [] : [ 'tests/phan/stubs/mail.php' ], - class_exists( Memcached::class ) ? [] : [ 'tests/phan/stubs/memcached.php' ], - // Per composer.json, PHPUnit 6 is used for PHP 7.0+, PHPUnit 4 otherwise. - // Load the interface for the version of PHPUnit that isn't installed. - // Phan only supports PHP 7.0+ (and not HHVM), so we only need to stub PHPUnit 4. - class_exists( PHPUnit_TextUI_Command::class ) ? [] : [ 'tests/phan/stubs/phpunit4.php' ], - class_exists( ProfilerExcimer::class ) ? [] : [ 'tests/phan/stubs/excimer.php' ], - [ - 'maintenance/7zip.inc', - 'maintenance/cleanupTable.inc', - 'maintenance/CodeCleanerGlobalsPass.inc', - 'maintenance/commandLine.inc', - 'maintenance/sqlite.inc', - 'maintenance/userDupes.inc', - 'maintenance/language/checkLanguage.inc', - 'maintenance/language/languages.inc', - ] - ), - - /** - * A list of directories that should be parsed for class and - * method information. After excluding the directories - * defined in exclude_analysis_directory_list, the remaining - * files will be statically analyzed for errors. - * - * Thus, both first-party and third-party code being used by - * your application should be included in this list. - */ - 'directory_list' => [ - 'includes/', - 'languages/', - 'maintenance/', - 'mw-config/', - 'resources/', - 'vendor/', - ], - - /** - * A file list that defines files that will be excluded - * from parsing and analysis and will not be read at all. - * - * This is useful for excluding hopelessly unanalyzable - * files that can't be removed for whatever reason. - */ - 'exclude_file_list' => [], - - /** - * A list of directories holding code that we want - * to parse, but not analyze. Also works for individual - * files. - */ - "exclude_analysis_directory_list" => [ - 'vendor/', - 'tests/phan/stubs/', - // The referenced classes are not available in vendor, only when - // included from composer. - 'includes/composer/', - // Directly references classes that only exist in Translate extension - 'maintenance/language/', - // External class - 'includes/libs/jsminplus.php', - ], - - /** - * Backwards Compatibility Checking. This is slow - * and expensive, but you should consider running - * it before upgrading your version of PHP to a - * new version that has backward compatibility - * breaks. - */ - 'backward_compatibility_checks' => false, - - /** - * A set of fully qualified class-names for which - * a call to parent::__construct() is required - */ - 'parent_constructor_required' => [ - ], - - /** - * Run a quick version of checks that takes less - * time at the cost of not running as thorough - * an analysis. You should consider setting this - * to true only when you wish you had more issues - * to fix in your code base. - * - * In quick-mode the scanner doesn't rescan a function - * or a method's code block every time a call is seen. - * This means that the problem here won't be detected: - * - * ```php - * false, - - /** - * By default, Phan will not analyze all node types - * in order to save time. If this config is set to true, - * Phan will dig deeper into the AST tree and do an - * analysis on all nodes, possibly finding more issues. - * - * See \Phan\Analysis::shouldVisit for the set of skipped - * nodes. - */ - 'should_visit_all_nodes' => true, - - /** - * If enabled, check all methods that override a - * parent method to make sure its signature is - * compatible with the parent's. This check - * can add quite a bit of time to the analysis. - */ - 'analyze_signature_compatibility' => true, - - // Emit all issues. They are then suppressed via - // suppress_issue_types, rather than a minimum - // severity. - "minimum_severity" => 0, - - /** - * If true, missing properties will be created when - * they are first seen. If false, we'll report an - * error message if there is an attempt to write - * to a class property that wasn't explicitly - * defined. - */ - 'allow_missing_properties' => false, - - /** - * Allow null to be cast as any type and for any - * type to be cast to null. Setting this to false - * will cut down on false positives. - */ - 'null_casts_as_any_type' => true, - - /** - * If enabled, scalars (int, float, bool, string, null) - * are treated as if they can cast to each other. - * - * MediaWiki is pretty lax and uses many scalar - * types interchangably. - */ - 'scalar_implicit_cast' => true, - - /** - * If true, seemingly undeclared variables in the global - * scope will be ignored. This is useful for projects - * with complicated cross-file globals that you have no - * hope of fixing. - */ - 'ignore_undeclared_variables_in_global_scope' => true, - - /** - * Set to true in order to attempt to detect dead - * (unreferenced) code. Keep in mind that the - * results will only be a guess given that classes, - * properties, constants and methods can be referenced - * as variables (like `$class->$property` or - * `$class->$method()`) in ways that we're unable - * to make sense of. - */ - 'dead_code_detection' => false, - - /** - * If true, the dead code detection rig will - * prefer false negatives (not report dead code) to - * false positives (report dead code that is not - * actually dead) which is to say that the graph of - * references will create too many edges rather than - * too few edges when guesses have to be made about - * what references what. - */ - 'dead_code_detection_prefer_false_negative' => true, - - /** - * If disabled, Phan will not read docblock type - * annotation comments (such as for @return, @param, - * @var, @suppress, @deprecated) and only rely on - * types expressed in code. - */ - 'read_type_annotations' => true, - - /** - * If a file path is given, the code base will be - * read from and written to the given location in - * order to attempt to save some work from being - * done. Only changed files will get analyzed if - * the file is read - */ - 'stored_state_file_path' => null, - - /** - * Set to true in order to ignore issue suppression. - * This is useful for testing the state of your code, but - * unlikely to be useful outside of that. - */ - 'disable_suppression' => false, - - /** - * If set to true, we'll dump the AST instead of - * analyzing files - */ - 'dump_ast' => false, - - /** - * If set to a string, we'll dump the fully qualified lowercase - * function and method signatures instead of analyzing files. - */ - 'dump_signatures_file' => null, - - /** - * If true (and if stored_state_file_path is set) we'll - * look at the list of files passed in and expand the list - * to include files that depend on the given files - */ - 'expand_file_list' => false, - - // Include a progress bar in the output - 'progress_bar' => false, - - /** - * The probability of actually emitting any progress - * bar update. Setting this to something very low - * is good for reducing network IO and filling up - * your terminal's buffer when running phan on a - * remote host. - */ - 'progress_bar_sample_rate' => 0.005, - - /** - * The number of processes to fork off during the analysis - * phase. - */ - 'processes' => 1, - - /** - * Add any issue types (such as 'PhanUndeclaredMethod') - * to this black-list to inhibit them from being reported. - */ - 'suppress_issue_types' => [ - // approximate error count: 29 - "PhanCommentParamOnEmptyParamList", - // approximate error count: 33 - "PhanCommentParamWithoutRealParam", - // approximate error count: 8 - "PhanDeprecatedClass", - // approximate error count: 415 - "PhanDeprecatedFunction", - // approximate error count: 25 - "PhanDeprecatedProperty", - // approximate error count: 17 - "PhanNonClassMethodCall", - // approximate error count: 888 - "PhanParamSignatureMismatch", - // approximate error count: 7 - "PhanParamSignatureMismatchInternal", - // approximate error count: 1 - "PhanParamSignatureRealMismatchTooFewParameters", - // approximate error count: 125 - "PhanParamTooMany", - // approximate error count: 3 - "PhanParamTooManyInternal", - // approximate error count: 1 - "PhanRedefineFunctionInternal", - // approximate error count: 2 - "PhanTraitParentReference", - // approximate error count: 3 - "PhanTypeComparisonFromArray", - // approximate error count: 2 - "PhanTypeComparisonToArray", - // approximate error count: 218 - "PhanTypeMismatchArgument", - // approximate error count: 13 - "PhanTypeMismatchArgumentInternal", - // approximate error count: 5 - "PhanTypeMismatchDimAssignment", - // approximate error count: 2 - "PhanTypeMismatchDimEmpty", - // approximate error count: 1 - "PhanTypeMismatchDimFetch", - // approximate error count: 14 - "PhanTypeMismatchForeach", - // approximate error count: 56 - "PhanTypeMismatchProperty", - // approximate error count: 74 - "PhanTypeMismatchReturn", - // approximate error count: 5 - "PhanTypeNonVarPassByRef", - // approximate error count: 32 - "PhanUndeclaredConstant", - // approximate error count: 233 - "PhanUndeclaredMethod", - // approximate error count: 1224 - "PhanUndeclaredProperty", - // approximate error count: 58 - "PhanUndeclaredVariableDim", - ], - - /** - * If empty, no filter against issues types will be applied. - * If this white-list is non-empty, only issues within the list - * will be emitted by Phan. - */ - 'whitelist_issue_types' => [ - // 'PhanAccessMethodPrivate', - // 'PhanAccessMethodProtected', - // 'PhanAccessNonStaticToStatic', - // 'PhanAccessPropertyPrivate', - // 'PhanAccessPropertyProtected', - // 'PhanAccessSignatureMismatch', - // 'PhanAccessSignatureMismatchInternal', - // 'PhanAccessStaticToNonStatic', - // 'PhanCompatibleExpressionPHP7', - // 'PhanCompatiblePHP7', - // 'PhanContextNotObject', - // 'PhanDeprecatedClass', - // 'PhanDeprecatedFunction', - // 'PhanDeprecatedProperty', - // 'PhanEmptyFile', - // 'PhanNonClassMethodCall', - // 'PhanNoopArray', - // 'PhanNoopClosure', - // 'PhanNoopConstant', - // 'PhanNoopProperty', - // 'PhanNoopVariable', - // 'PhanParamRedefined', - // 'PhanParamReqAfterOpt', - // 'PhanParamSignatureMismatch', - // 'PhanParamSignatureMismatchInternal', - // 'PhanParamSpecial1', - // 'PhanParamSpecial2', - // 'PhanParamSpecial3', - // 'PhanParamSpecial4', - // 'PhanParamTooFew', - // 'PhanParamTooFewInternal', - // 'PhanParamTooMany', - // 'PhanParamTooManyInternal', - // 'PhanParamTypeMismatch', - // 'PhanParentlessClass', - // 'PhanRedefineClass', - // 'PhanRedefineClassInternal', - // 'PhanRedefineFunction', - // 'PhanRedefineFunctionInternal', - // 'PhanStaticCallToNonStatic', - // 'PhanSyntaxError', - // 'PhanTraitParentReference', - // 'PhanTypeArrayOperator', - // 'PhanTypeArraySuspicious', - // 'PhanTypeComparisonFromArray', - // 'PhanTypeComparisonToArray', - // 'PhanTypeConversionFromArray', - // 'PhanTypeInstantiateAbstract', - // 'PhanTypeInstantiateInterface', - // 'PhanTypeInvalidLeftOperand', - // 'PhanTypeInvalidRightOperand', - // 'PhanTypeMismatchArgument', - // 'PhanTypeMismatchArgumentInternal', - // 'PhanTypeMismatchDefault', - // 'PhanTypeMismatchForeach', - // 'PhanTypeMismatchProperty', - // 'PhanTypeMismatchReturn', - // 'PhanTypeMissingReturn', - // 'PhanTypeNonVarPassByRef', - // 'PhanTypeParentConstructorCalled', - // 'PhanTypeVoidAssignment', - // 'PhanUnanalyzable', - // 'PhanUndeclaredClass', - // 'PhanUndeclaredClassCatch', - // 'PhanUndeclaredClassConstant', - // 'PhanUndeclaredClassInstanceof', - // 'PhanUndeclaredClassMethod', - // 'PhanUndeclaredClassReference', - // 'PhanUndeclaredConstant', - // 'PhanUndeclaredExtendedClass', - // 'PhanUndeclaredFunction', - // 'PhanUndeclaredInterface', - // 'PhanUndeclaredMethod', - // 'PhanUndeclaredProperty', - // 'PhanUndeclaredStaticMethod', - // 'PhanUndeclaredStaticProperty', - // 'PhanUndeclaredTrait', - // 'PhanUndeclaredTypeParameter', - // 'PhanUndeclaredTypeProperty', - // 'PhanUndeclaredVariable', - // 'PhanUnreferencedClass', - // 'PhanUnreferencedConstant', - // 'PhanUnreferencedMethod', - // 'PhanUnreferencedProperty', - // 'PhanVariableUseClause', - ], - - /** - * Override to hardcode existence and types of (non-builtin) globals in the global scope. - * Class names must be prefixed with '\\'. - * (E.g. ['_FOO' => '\\FooClass', 'page' => '\\PageClass', 'userId' => 'int']) - */ - 'globals_type_map' => [ - 'IP' => 'string', - ], - - // Emit issue messages with markdown formatting - 'markdown_issue_messages' => false, - - /** - * Enable or disable support for generic templated - * class types. - */ - 'generic_types_enabled' => true, - - // A list of plugin files to execute - 'plugins' => [ - ], -]; diff --git a/tests/phan/stubs/README b/tests/phan/stubs/README deleted file mode 100644 index c458ab58fe..0000000000 --- a/tests/phan/stubs/README +++ /dev/null @@ -1,3 +0,0 @@ -These stubs describe how code that is not available at analysis time should be -used. No implementations are necessary, just define the classes and their -methods and use phpdoc to describe what arguments are allowed. diff --git a/tests/phan/stubs/excimer.php b/tests/phan/stubs/excimer.php deleted file mode 100644 index af3a67398c..0000000000 --- a/tests/phan/stubs/excimer.php +++ /dev/null @@ -1,86 +0,0 @@ -addToAssertionCount( 1 ); } } diff --git a/tests/phpunit/LessFileCompilationTest.php b/tests/phpunit/LessFileCompilationTest.php index 5e1f1a9666..1f1c395a1a 100644 --- a/tests/phpunit/LessFileCompilationTest.php +++ b/tests/phpunit/LessFileCompilationTest.php @@ -5,6 +5,7 @@ * * @see https://github.com/sebastianbergmann/phpunit/blob/master/src/Extensions/PhptTestCase.php * @author Sam Smith + * @coversNothing */ class LessFileCompilationTest extends ResourceLoaderTestCase { diff --git a/tests/phpunit/Makefile b/tests/phpunit/Makefile index d34e1836b2..26d52179ec 100644 --- a/tests/phpunit/Makefile +++ b/tests/phpunit/Makefile @@ -39,7 +39,7 @@ tap: ${PU} --tap coverage: - ${PU} --coverage-html ../../docs/code-coverage + ${PU} --coverage-html ../../docs/coverage parser: ${PU} --group Parser diff --git a/tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php b/tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php new file mode 100644 index 0000000000..8f3418077b --- /dev/null +++ b/tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php @@ -0,0 +1,101 @@ +lastTestLogs = null; + $this->originalSpi = LoggerFactory::getProvider(); + $this->spi = new LogCapturingSpi( $this->originalSpi ); + LoggerFactory::registerProvider( $this->spi ); + } + + public function addRiskyTest( PHPUnit_Framework_Test $test, Exception $e, $time ) { + $this->augmentTestWithLogs( $test ); + } + + public function addIncompleteTest( PHPUnit_Framework_Test $test, Exception $e, $time ) { + $this->augmentTestWithLogs( $test ); + } + + public function addSkippedTest( PHPUnit_Framework_Test $test, Exception $e, $time ) { + $this->augmentTestWithLogs( $test ); + } + + public function addError( PHPUnit_Framework_Test $test, Exception $e, $time ) { + $this->augmentTestWithLogs( $test ); + } + + public function addWarning( PHPUnit_Framework_Test $test, PHPUnit\Framework\Warning $e, $time ) { + $this->augmentTestWithLogs( $test ); + } + + public function addFailure( PHPUnit_Framework_Test $test, + PHPUnit_Framework_AssertionFailedError $e, $time + ) { + $this->augmentTestWithLogs( $test ); + } + + private function augmentTestWithLogs( PHPUnit_Framework_Test $test ) { + if ( $this->spi ) { + $logs = $this->spi->getLogs(); + $formatted = $this->formatLogs( $logs ); + $test->_formattedMediaWikiLogs = $formatted; + } + } + + /** + * A test ended. + * + * @param PHPUnit_Framework_Test $test + * @param float $time + */ + public function endTest( PHPUnit_Framework_Test $test, $time ) { + LoggerFactory::registerProvider( $this->originalSpi ); + $this->originalSpi = null; + $this->spi = null; + } + + /** + * Get string formatted logs generated during the last + * test to execute. + * + * @param array $logs + * @return string + */ + private function formatLogs( array $logs ) { + $message = []; + foreach ( $logs as $log ) { + if ( $log['channel'] === 'PHPUnitCommand' ) { + // Don't print the log of PHPUnit events while running PHPUnit, + // because PHPUnit is already printing those already. + continue; + } + $message[] = sprintf( + '[%s] [%s] %s %s', + $log['channel'], + $log['level'], + $log['message'], + json_encode( $log['context'] ) + ); + } + return implode( "\n", $message ); + } +} diff --git a/tests/phpunit/MediaWikiPHPUnitCommand.php b/tests/phpunit/MediaWikiPHPUnitCommand.php index 897919541d..6b1d8177e5 100644 --- a/tests/phpunit/MediaWikiPHPUnitCommand.php +++ b/tests/phpunit/MediaWikiPHPUnitCommand.php @@ -18,11 +18,18 @@ class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command { $this->arguments['configuration'] = __DIR__ . '/suite.xml'; } - // Add our own listener + // Add our own listeners $this->arguments['listeners'][] = new MediaWikiPHPUnitTestListener; + $this->arguments['listeners'][] = new MediaWikiLoggerPHPUnitTestListener; // Output only to stderr to avoid "Headers already sent" problems $this->arguments['stderr'] = true; + + // Use a custom result printer that includes per-test logging output + // when nothing is provided. + if ( !isset( $this->arguments['printer'] ) ) { + $this->arguments['printer'] = MediaWikiPHPUnitResultPrinter::class; + } } protected function createRunner() { diff --git a/tests/phpunit/MediaWikiPHPUnitResultPrinter.php b/tests/phpunit/MediaWikiPHPUnitResultPrinter.php new file mode 100644 index 0000000000..d0ac8ffa1e --- /dev/null +++ b/tests/phpunit/MediaWikiPHPUnitResultPrinter.php @@ -0,0 +1,14 @@ +failedTest(); + if ( $test !== null && isset( $test->_formattedMediaWikiLogs ) ) { + $log = $test->_formattedMediaWikiLogs; + if ( $log ) { + $this->write( "=== Logs generated by test case\n{$log}\n===\n" ); + } + } + parent::printDefectTrace( $defect ); + } +} diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index 287d28c3b9..8a38f42b41 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -3,6 +3,7 @@ use MediaWiki\Logger\LegacySpi; use MediaWiki\Logger\LoggerFactory; use MediaWiki\Logger\MonologSpi; +use MediaWiki\Logger\LogCapturingSpi; use MediaWiki\MediaWikiServices; use Psr\Log\LoggerInterface; use Wikimedia\Rdbms\IDatabase; @@ -360,6 +361,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { public static function resetNonServiceCaches() { global $wgRequest, $wgJobClasses; + User::resetGetDefaultOptionsForTestsOnly(); foreach ( $wgJobClasses as $type => $class ) { JobQueueGroup::singleton()->get( $type )->delete(); } @@ -399,7 +401,8 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); self::$reuseDB = $this->getCliArg( 'reuse-db' ); - $this->db = wfGetDB( DB_MASTER ); + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $this->db = $lb->getConnection( DB_MASTER ); $this->checkDbIsSupported(); @@ -1124,7 +1127,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $this->loggers[$channel] = $singletons['loggers'][$channel] ?? null; } $singletons['loggers'][$channel] = $logger; - } elseif ( $provider instanceof LegacySpi ) { + } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) { if ( !isset( $this->loggers[$channel] ) ) { $this->loggers[$channel] = $singletons[$channel] ?? null; } @@ -1151,7 +1154,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } else { $singletons['loggers'][$channel] = $logger; } - } elseif ( $provider instanceof LegacySpi ) { + } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) { if ( $logger === null ) { unset( $singletons[$channel] ); } else { @@ -1362,6 +1365,9 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { JobQueueGroup::singleton()->get( $type )->delete(); } + // T219673: close any connections from code that failed to call reuseConnection() + // or is still holding onto a DBConnRef instance (e.g. in a singleton). + MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->closeAll(); CloneDatabase::changePrefix( self::$oldTablePrefix ); self::$oldTablePrefix = false; @@ -1452,12 +1458,12 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @note this method only works when first called. Subsequent calls have no effect, * even if using different parameters. * - * @param Database $db The database connection + * @param IMaintainableDatabase $db The database connection * @param string $prefix The prefix to use for the new table set (aka schema). * * @throws MWException If the database table prefix is already $prefix */ - public static function setupTestDB( Database $db, $prefix ) { + public static function setupTestDB( IMaintainableDatabase $db, $prefix ) { if ( self::$dbSetup ) { return; } @@ -1594,7 +1600,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $this->ensureMockDatabaseConnection( $db ); $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults; - $originalTables = $this->listOriginalTables( $db, 'unprefixed' ); + $originalTables = $this->listOriginalTables( $db ); // Drop tables that need to be restored or removed. $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] ); @@ -1655,7 +1661,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $this->ensureMockDatabaseConnection( $db ); // Drop the tables that will be created by the schema scripts. - $originalTables = $this->listOriginalTables( $db, 'unprefixed' ); + $originalTables = $this->listOriginalTables( $db ); $tablesToDrop = array_intersect( $originalTables, $overrides['create'] ); if ( $tablesToDrop ) { @@ -1700,29 +1706,36 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } /** - * Lists all tables in the live database schema. + * Lists all tables in the live database schema, without a prefix. * * @param IMaintainableDatabase $db - * @param string $prefix Either 'prefixed' or 'unprefixed' * @return array */ - private function listOriginalTables( IMaintainableDatabase $db, $prefix = 'prefixed' ) { + private function listOriginalTables( IMaintainableDatabase $db ) { if ( !isset( $db->_originalTablePrefix ) ) { throw new LogicException( 'No original table prefix know, cannot list tables!' ); } $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ ); - if ( $prefix === 'unprefixed' ) { - $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/'; - $originalTables = array_map( - function ( $pt ) use ( $originalPrefixRegex ) { - return preg_replace( $originalPrefixRegex, '', $pt ); - }, - $originalTables - ); - } - return $originalTables; + $unittestPrefixRegex = '/^' . preg_quote( $this->dbPrefix(), '/' ) . '/'; + $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/'; + + $originalTables = array_filter( + $originalTables, + function ( $pt ) use ( $unittestPrefixRegex ) { + return !preg_match( $unittestPrefixRegex, $pt ); + } + ); + + $originalTables = array_map( + function ( $pt ) use ( $originalPrefixRegex ) { + return preg_replace( $originalPrefixRegex, '', $pt ); + }, + $originalTables + ); + + return array_unique( $originalTables ); } /** @@ -1740,7 +1753,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { throw new LogicException( 'No original table prefix know, cannot restore tables!' ); } - $originalTables = $this->listOriginalTables( $db, 'unprefixed' ); + $originalTables = $this->listOriginalTables( $db ); $tables = array_intersect( $tables, $originalTables ); $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix ); @@ -1888,7 +1901,17 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @param IDatabase $target */ public function copyTestData( IDatabase $source, IDatabase $target ) { - $tables = self::listOriginalTables( $source, 'unprefixed' ); + if ( $this->db->getType() === 'sqlite' ) { + // SQLite uses a non-temporary copy of the searchindex table for testing, + // which gets deleted and re-created when setting up the secondary connection, + // causing "Error 17" when trying to copy the data. See T191863#4130112. + throw new RuntimeException( + 'Setting up a secondary database connection with test data is currently not' + . 'with SQLite. You may want to use markTestSkippedIfDbType() to bypass this issue.' + ); + } + + $tables = self::listOriginalTables( $source ); foreach ( $tables as $table ) { $res = $source->select( $table, '*', [], __METHOD__ ); diff --git a/tests/phpunit/PHPUnit4And6Compat.php b/tests/phpunit/PHPUnit4And6Compat.php index 672ab4a4d6..1ef0c91683 100644 --- a/tests/phpunit/PHPUnit4And6Compat.php +++ b/tests/phpunit/PHPUnit4And6Compat.php @@ -29,9 +29,9 @@ trait PHPUnit4And6Compat { * is a temporary backwards-compatibility layer while we transition. */ public function setExpectedException( $name, $message = '', $code = null ) { - if ( is_callable( [ $this, 'expectException' ] ) ) { + if ( is_callable( 'parent::expectException' ) ) { if ( $name !== null ) { - $this->expectException( $name ); + parent::expectException( $name ); } if ( $message !== '' ) { $this->expectExceptionMessage( $message ); @@ -44,6 +44,18 @@ trait PHPUnit4And6Compat { } } + /** + * Future-compatible layer for PHPUnit 4's setExpectedException. + */ + public function expectException( $exception ) { + if ( is_callable( 'parent::expectException' ) ) { + parent::expectException( $exception ); + return; + } + + parent::setExpectedException( $exception ); + } + /** * @see PHPUnit_Framework_TestCase::getMock * @@ -118,4 +130,18 @@ trait PHPUnit4And6Compat { // ->disallowMockingUnknownTypes() ->getMock(); } + + /** + * Marks the current test as risky. This + * is a forward port of the markAsRisky function that + * was introduced in PHPUnit 5.7.6. + */ + public function markAsRisky() { + if ( is_callable( 'parent::markAsRisky' ) ) { + return parent::markAsRisky(); + } + + // "risky" tests are not supported in phpunit 4, so just ignore + } + } diff --git a/tests/phpunit/ResourceLoaderTestCase.php b/tests/phpunit/ResourceLoaderTestCase.php index cadd0ff3c7..c7fb48b6f3 100644 --- a/tests/phpunit/ResourceLoaderTestCase.php +++ b/tests/phpunit/ResourceLoaderTestCase.php @@ -26,6 +26,7 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase { $options = [ 'lang' => $options ]; } $options += [ + 'debug' => 'true', 'lang' => 'en', 'dir' => 'ltr', 'skin' => 'vector', @@ -35,6 +36,7 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase { ]; $resourceLoader = $rl ?: new ResourceLoader(); $request = new FauxRequest( [ + 'debug' => $options['debug'], 'lang' => $options['lang'], 'modules' => $options['modules'], 'only' => $options['only'], @@ -58,9 +60,6 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase { // Avoid influence from wgInvalidateCacheOnLocalSettingsChange 'CacheEpoch' => '20140101000000', - // For ResourceLoader::__construct() - 'ResourceLoaderSources' => [], - // For wfScript() 'ScriptPath' => '/w', 'Script' => '/w/index.php', diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index a5c8ef61c4..79cb5be338 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -14,16 +14,21 @@ EOF; require_once __DIR__ . "/phpunit.php"; } -class MediaWikiPHPUnitBootstrap { - public function __destruct() { - // Return to real wiki db, so profiling data is preserved - MediaWikiTestCase::teardownTestDB(); +// The PHPUnit_TextUI_TestRunner class will run each test suite and may call +// exit() with an exit status code. As such, we cannot run code "after the last test" +// by adding statements to PHPUnitMaintClass::execute or MediaWikiPHPUnitCommand::run. +// Instead, we work around it by registering a shutdown callback from the bootstrap +// file, which runs before PHPUnit starts. +// @todo Once we use PHPUnit 8 or higher, use the 'AfterLastTestHook' feature. +// https://phpunit.readthedocs.io/en/8.0/extending-phpunit.html#available-hook-interfaces +register_shutdown_function( function () { + // This will: + // - clear the temporary job queue. + // - allow extensions to delete any temporary tables they created. + // - restore ability to connect to the real database, + // (for logging profiling data). + MediaWikiTestCase::teardownTestDB(); - // Log profiling data, e.g. in the database or UDP - wfLogProfilingData(); - } - -} - -// This will be destructed after all tests have been run -$mediawikiPHPUnitBootstrap = new MediaWikiPHPUnitBootstrap(); + // Log profiling data, e.g. in the database or UDP + wfLogProfilingData(); +} ); diff --git a/tests/phpunit/data/media/translated.svg b/tests/phpunit/data/media/translated.svg new file mode 100644 index 0000000000..afd9fb4ef4 --- /dev/null +++ b/tests/phpunit/data/media/translated.svg @@ -0,0 +1,10 @@ + + + + + RU + DE + fallback + + + diff --git a/tests/phpunit/data/registration/good.json b/tests/phpunit/data/registration/good.json index cfad069c17..8c024661a4 100644 --- a/tests/phpunit/data/registration/good.json +++ b/tests/phpunit/data/registration/good.json @@ -1,5 +1,7 @@ { "name": "FooBar", + "@note": "This is a note", + "@duck": "Docs say any @-item is ignored", "attributes": { "FooBar": { "Attr": [ "test" ] @@ -8,5 +10,12 @@ "Attr": [ "test2" ] } }, + "config": { + "MyConfigValue": { + "value": 42, + "description": "Very important config value", + "public": true + } + }, "manifest_version": 2 } diff --git a/tests/phpunit/data/resourceloader/sample.json b/tests/phpunit/data/resourceloader/sample.json new file mode 100644 index 0000000000..f2b69d076d --- /dev/null +++ b/tests/phpunit/data/resourceloader/sample.json @@ -0,0 +1,4 @@ +{ + "foo": "bar", + "answer": 42 +} diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php index 8288cae0f2..ec806aeff4 100644 --- a/tests/phpunit/docs/ExportDemoTest.php +++ b/tests/phpunit/docs/ExportDemoTest.php @@ -6,6 +6,7 @@ * * @group Dump * @group large + * @coversNothing */ class ExportDemoTest extends DumpTestCase { diff --git a/tests/phpunit/documentation/ReleaseNotesTest.php b/tests/phpunit/documentation/ReleaseNotesTest.php index 2789571e2b..d20fcff7b8 100644 --- a/tests/phpunit/documentation/ReleaseNotesTest.php +++ b/tests/phpunit/documentation/ReleaseNotesTest.php @@ -34,15 +34,20 @@ class ReleaseNotesTest extends MediaWikiTestCase { } // Also test the README and similar files - $otherFiles = [ "$IP/COPYING", "$IP/FAQ", "$IP/INSTALL", "$IP/README", "$IP/SECURITY" ]; + $otherFiles = [ + "$IP/COPYING", + "$IP/FAQ", + "$IP/HISTORY", + "$IP/INSTALL", + "$IP/README", + "$IP/SECURITY" + ]; foreach ( $otherFiles as $index => $fileName ) { $this->assertFileLength( "Help", $fileName ); } } - /** - */ private function assertFileLength( $type, $fileName ) { $file = file( $fileName, FILE_IGNORE_NEW_LINES ); @@ -51,16 +56,13 @@ class ReleaseNotesTest extends MediaWikiTestCase { "$type file '$fileName' is inaccessible." ); - $lines = count( $file ); - - for ( $i = 0; $i < $lines; $i++ ) { - $line = $file[$i]; - + foreach ( $file as $i => $line ) { + $num = $i + 1; $this->assertLessThanOrEqual( // FILE_IGNORE_NEW_LINES drops the \n at the EOL, so max length is 80 not 81. 80, mb_strlen( $line ), - "$type file '$fileName' line $i is longer than 80 chars:\n\t'$line'" + "$type file '$fileName' line $num, is longer than 80 chars:\n\t'$line'" ); } } diff --git a/tests/phpunit/includes/ActorMigrationTest.php b/tests/phpunit/includes/ActorMigrationTest.php index b761d29758..1f2b13cfa8 100644 --- a/tests/phpunit/includes/ActorMigrationTest.php +++ b/tests/phpunit/includes/ActorMigrationTest.php @@ -571,10 +571,8 @@ class ActorMigrationTest extends MediaWikiLangTestCase { public static function provideInsertRoundTrip() { $db = wfGetDB( DB_REPLICA ); // for timestamps - $ipbfields = [ - ]; - $revfields = [ - ]; + $comment = MediaWikiServices::getInstance()->getCommentStore() + ->createComment( wfGetDB( DB_MASTER ), '' ); return [ 'recentchanges' => [ 'recentchanges', 'rc_user', 'rc_id', [ @@ -584,16 +582,17 @@ class ActorMigrationTest extends MediaWikiLangTestCase { 'rc_this_oldid' => 42, 'rc_last_oldid' => 41, 'rc_source' => 'test', + 'rc_comment_id' => $comment->id, ] ], 'ipblocks' => [ 'ipblocks', 'ipb_by', 'ipb_id', [ 'ipb_range_start' => '', 'ipb_range_end' => '', 'ipb_timestamp' => $db->timestamp(), 'ipb_expiry' => $db->getInfinity(), + 'ipb_reason_id' => $comment->id, ] ], 'revision' => [ 'revision', 'rev_user', 'rev_id', [ 'rev_page' => 42, - 'rev_text_id' => 42, 'rev_len' => 0, 'rev_timestamp' => $db->timestamp(), ] ], @@ -679,7 +678,6 @@ class ActorMigrationTest extends MediaWikiLangTestCase { $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $userIdentity ); $extraFields = [ 'rev_page' => 42, - 'rev_text_id' => 42, 'rev_len' => 0, 'rev_timestamp' => $this->db->timestamp(), ] + $cFields; diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php index e4dce12801..7874688e7b 100644 --- a/tests/phpunit/includes/BlockTest.php +++ b/tests/phpunit/includes/BlockTest.php @@ -94,7 +94,7 @@ class BlockTest extends MediaWikiLangTestCase { $madeAt = wfTimestamp( TS_MW ); // delta to stop one-off errors when things happen to go over a second mark. - $delta = abs( $madeAt - $block->mTimestamp ); + $delta = abs( $madeAt - $block->getTimestamp() ); $this->assertLessThan( 2, $delta, @@ -131,7 +131,7 @@ class BlockTest extends MediaWikiLangTestCase { } /** - * @covers Block::prevents + * @covers Block::appliesToRight */ public function testBlockedUserCanNotCreateAccount() { $username = 'BlockedUserToCreateAccountWith'; @@ -174,8 +174,8 @@ class BlockTest extends MediaWikiLangTestCase { // Reload block from DB $userBlock = Block::newFromTarget( $username ); $this->assertTrue( - (bool)$block->prevents( 'createaccount' ), - "Block object in DB should prevents 'createaccount'" + (bool)$block->appliesToRight( 'createaccount' ), + "Block object in DB should block right 'createaccount'" ); $this->assertInstanceOf( @@ -302,9 +302,9 @@ class BlockTest extends MediaWikiLangTestCase { $block = new Block(); $block->setTarget( $target ); $block->setBlocker( $blocker ); - $block->mReason = $insBlock['desc']; - $block->mExpiry = 'infinity'; - $block->prevents( 'createaccount', $insBlock['ACDisable'] ); + $block->setReason( $insBlock['desc'] ); + $block->setExpiry( 'infinity' ); + $block->isCreateAccountBlocked( $insBlock['ACDisable'] ); $block->isHardblock( $insBlock['isHardblock'] ); $block->isAutoblocking( $insBlock['isAutoBlocking'] ); $block->insert(); @@ -369,61 +369,8 @@ class BlockTest extends MediaWikiLangTestCase { $xffblocks = Block::getBlocksForIPList( $list, true ); $this->assertEquals( $exCount, count( $xffblocks ), 'Number of blocks for ' . $xff ); $block = Block::chooseBlock( $xffblocks, $list ); - $this->assertEquals( $exResult, $block->mReason, 'Correct block type for XFF header ' . $xff ); - } - - /** - * @covers Block::__construct - */ - public function testDeprecatedConstructor() { - $this->hideDeprecated( 'Block::__construct with multiple arguments' ); - $username = 'UnthinkablySecretRandomUsername'; - $reason = 'being irrational'; - - # Set up the target - $u = User::newFromName( $username ); - if ( $u->getId() == 0 ) { - $u->addToDatabase(); - TestUser::setPasswordForUser( $u, 'TotallyObvious' ); - } - unset( $u ); - - # Make sure the user isn't blocked - $this->assertNull( - Block::newFromTarget( $username ), - "$username should not be blocked" - ); - - # Perform the block - $block = new Block( - /* address */ $username, - /* user */ 0, - /* by */ $this->getTestSysop()->getUser()->getId(), - /* reason */ $reason, - /* timestamp */ 0, - /* auto */ false, - /* expiry */ 0 - ); - $block->insert(); - - # Check target $this->assertEquals( - $block->getTarget()->getName(), - $username, - "Target should be set properly" - ); - - # Check supplied parameter - $this->assertEquals( - $block->mReason, - $reason, - "Reason should be non-default" - ); - - # Check default parameter - $this->assertFalse( - (bool)$block->prevents( 'createaccount' ), - "Account creation should not be blocked by default" + $exResult, $block->getReason(), 'Correct block type for XFF header ' . $xff ); } @@ -616,6 +563,9 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::appliesToTitle */ public function testAppliesToTitleReturnsTrueOnSitewideBlock() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $user = $this->getTestUser()->getUser(); $block = new Block( [ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ), @@ -642,6 +592,9 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::appliesToTitle */ public function testAppliesToTitleOnPartialBlock() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $user = $this->getTestUser()->getUser(); $block = new Block( [ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ), @@ -673,6 +626,9 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::appliesToPage */ public function testAppliesToReturnsTrueOnSitewideBlock() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $user = $this->getTestUser()->getUser(); $block = new Block( [ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ), @@ -697,6 +653,9 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::appliesToPage */ public function testAppliesToPageOnPartialPageBlock() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $user = $this->getTestUser()->getUser(); $block = new Block( [ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ), @@ -725,6 +684,9 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::appliesToNamespace */ public function testAppliesToNamespaceOnPartialNamespaceBlock() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $user = $this->getTestUser()->getUser(); $block = new Block( [ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ), @@ -745,4 +707,15 @@ class BlockTest extends MediaWikiLangTestCase { $block->delete(); } + /** + * @covers Block::appliesToRight + */ + public function testBlockAllowsPurge() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); + $block = new Block(); + $this->assertFalse( $block->appliesToRight( 'purge' ) ); + } + } diff --git a/tests/phpunit/includes/CategoryTest.php b/tests/phpunit/includes/CategoryTest.php new file mode 100644 index 0000000000..60fb144a44 --- /dev/null +++ b/tests/phpunit/includes/CategoryTest.php @@ -0,0 +1,287 @@ +setMwGlobals( [ + 'wgAllowUserJs' => false, + 'wgDefaultLanguageVariant' => false, + 'wgMetaNamespace' => 'Project', + ] ); + $this->setUserLang( 'en' ); + $this->setContentLang( 'en' ); + } + + /** + * @covers Category::initialize() + */ + public function testInitialize_idNotExist() { + $category = Category::newFromID( -1 ); + $this->assertFalse( $category->getName() ); + } + + public function provideInitializeVariants() { + return [ + // Existing title + [ 'newFromName', 'Example', 'getID', 1 ], + [ 'newFromName', 'Example', 'getName', 'Example' ], + [ 'newFromName', 'Example', 'getPageCount', 3 ], + [ 'newFromName', 'Example', 'getSubcatCount', 4 ], + [ 'newFromName', 'Example', 'getFileCount', 5 ], + + // Non-existing title + [ 'newFromName', 'NoExample', 'getID', 0 ], + [ 'newFromName', 'NoExample', 'getName', 'NoExample' ], + [ 'newFromName', 'NoExample', 'getPageCount', 0 ], + [ 'newFromName', 'NoExample', 'getSubcatCount', 0 ], + [ 'newFromName', 'NoExample', 'getFileCount', 0 ], + + // Existing ID + [ 'newFromID', 1, 'getID', 1 ], + [ 'newFromID', 1, 'getName', 'Example' ], + [ 'newFromID', 1, 'getPageCount', 3 ], + [ 'newFromID', 1, 'getSubcatCount', 4 ], + [ 'newFromID', 1, 'getFileCount', 5 ] + ]; + } + + /** + * @covers Category::initialize() + * @dataProvider provideInitializeVariants + */ + public function testInitialize( $createFunction, $createParam, $testFunction, $expected ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $category = Category::{$createFunction}( $createParam ); + $this->assertEquals( $expected, $category->{$testFunction}() ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } + + /** + * @covers Category::newFromName() + * @covers Category::getName() + */ + public function testNewFromName_validTitle() { + $category = Category::newFromName( 'Example' ); + $this->assertSame( 'Example', $category->getName() ); + } + + /** + * @covers Category::newFromName() + */ + public function testNewFromName_invalidTitle() { + $this->assertFalse( Category::newFromName( '#' ) ); + } + + /** + * @covers Category::newFromTitle() + */ + public function testNewFromTitle() { + $title = Title::newFromText( 'Category:Example' ); + $category = Category::newFromTitle( $title ); + $this->assertSame( 'Example', $category->getName() ); + } + + /** + * @covers Category::newFromID() + * @covers Category::getID() + */ + public function testNewFromID() { + $category = Category::newFromID( 5 ); + $this->assertSame( 5, $category->getID() ); + } + + /** + * @covers Category::newFromRow() + */ + public function testNewFromRow_found() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $category = Category::newFromRow( $dbw->selectRow( + 'category', + [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ], + [ 'cat_id' => 1 ], + __METHOD__ + ) ); + + $this->assertEquals( 1, $category->getID() ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } + + /** + * @covers Category::newFromRow() + */ + public function testNewFromRow_notFoundWithoutTitle() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $row = $dbw->selectRow( + 'category', + [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ], + [ 'cat_id' => 1 ], + __METHOD__ + ); + $row->cat_title = null; + + $this->assertFalse( Category::newFromRow( $row ) ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } + + /** + * @covers Category::newFromRow() + */ + public function testNewFromRow_notFoundWithTitle() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $row = $dbw->selectRow( + 'category', + [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ], + [ 'cat_id' => 1 ], + __METHOD__ + ); + $row->cat_title = null; + + $category = Category::newFromRow( + $row, + Title::newFromText( NS_CATEGORY, 'Example' ) + ); + + $this->assertFalse( $category->getID() ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } + + /** + * @covers Category::getPageCount() + */ + public function testGetPageCount() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $category = Category::newFromID( 1 ); + $this->assertEquals( 3, $category->getPageCount() ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } + + /** + * @covers Category::getSubcatCount() + */ + public function testGetSubcatCount() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $category = Category::newFromID( 1 ); + $this->assertEquals( 4, $category->getSubcatCount() ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } + + /** + * @covers Category::getFileCount() + */ + public function testGetFileCount() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'category', + [ + [ + 'cat_id' => 1, + 'cat_title' => 'Example', + 'cat_pages' => 3, + 'cat_subcats' => 4, + 'cat_files' => 5 + ] + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $category = Category::newFromID( 1 ); + $this->assertEquals( 5, $category->getFileCount() ); + + $dbw->delete( 'category', '*', __METHOD__ ); + } +} diff --git a/tests/phpunit/includes/CommentStoreCommentTest.php b/tests/phpunit/includes/CommentStoreCommentTest.php new file mode 100644 index 0000000000..2dfe03ad6b --- /dev/null +++ b/tests/phpunit/includes/CommentStoreCommentTest.php @@ -0,0 +1,26 @@ +assertSame( $message, $comment->message ); + } + + public function testConstructorWithoutMessage() { + $text = '{{template|param}}'; + $comment = new CommentStoreComment( null, $text ); + + $this->assertSame( $text, $comment->message->text() ); + } + +} diff --git a/tests/phpunit/includes/CommentStoreTest.php b/tests/phpunit/includes/CommentStoreTest.php index 4360343981..9c08b9f94a 100644 --- a/tests/phpunit/includes/CommentStoreTest.php +++ b/tests/phpunit/includes/CommentStoreTest.php @@ -1,6 +1,7 @@ [ + __DIR__ . '/CommentStoreTest.sql', + ], + 'drop' => [], + 'create' => [ 'commentstore1', 'commentstore2', 'commentstore2_temp' ], + 'alter' => [], + ]; + } + /** * Create a store for a particular stage * @param int $stage @@ -25,6 +37,16 @@ class CommentStoreTest extends MediaWikiLangTestCase { */ protected function makeStore( $stage ) { $store = new CommentStore( MediaWikiServices::getInstance()->getContentLanguage(), $stage ); + + TestingAccessWrapper::newFromObject( $store )->tempTables += [ 'cs2_comment' => [ + 'table' => 'commentstore2_temp', + 'pk' => 'cs2t_id', + 'field' => 'cs2t_comment_id', + 'joinPK' => 'cs2_id', + 'stage' => MIGRATION_OLD, + 'deprecatedIn' => null, + ] ]; + return $store; } @@ -38,6 +60,16 @@ class CommentStoreTest extends MediaWikiLangTestCase { $this->hideDeprecated( 'CommentStore::newKey' ); $store = CommentStore::newKey( $key ); TestingAccessWrapper::newFromObject( $store )->stage = $stage; + + TestingAccessWrapper::newFromObject( $store )->tempTables += [ 'cs2_comment' => [ + 'table' => 'commentstore2_temp', + 'pk' => 'cs2t_id', + 'field' => 'cs2t_comment_id', + 'joinPK' => 'cs2_id', + 'stage' => MIGRATION_OLD, + 'deprecatedIn' => null, + ] ]; + return $store; } @@ -352,6 +384,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { "message keys $from" ); $this->assertEquals( $expect['message']->text(), $actual->message->text(), "message rendering $from" ); + $this->assertEquals( $expect['text'], $actual->message->text(), + "message rendering and text $from" ); $this->assertEquals( $expect['data'], $actual->data, "data $from" ); } @@ -360,15 +394,16 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @param string $table * @param string $key * @param string $pk - * @param string $extraFields * @param string|Message $comment * @param array|null $data * @param array $expect */ - public function testInsertRoundTrip( $table, $key, $pk, $extraFields, $comment, $data, $expect ) { + public function testInsertRoundTrip( $table, $key, $pk, $comment, $data, $expect ) { + static $id = 1; + $expectOld = [ 'text' => $expect['text'], - 'message' => new RawMessage( '$1', [ $expect['text'] ] ), + 'message' => new RawMessage( '$1', [ Message::plaintextParam( $expect['text'] ) ] ), 'data' => null, ]; @@ -381,12 +416,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { ]; foreach ( $stages as $writeStage => $possibleReadStages ) { - if ( $key === 'ipb_reason' ) { - $extraFields['ipb_address'] = __CLASS__ . "#$writeStage"; - } - $wstore = $this->makeStore( $writeStage ); - $usesTemp = $key === 'rev_comment'; + $usesTemp = $key === 'cs2_comment'; if ( $usesTemp ) { list( $fields, $callback ) = $wstore->insertWithTempTable( @@ -407,8 +438,7 @@ class CommentStoreTest extends MediaWikiLangTestCase { $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" ); } - $this->db->insert( $table, $extraFields + $fields, __METHOD__ ); - $id = $this->db->insertId(); + $this->db->insert( $table, [ $pk => ++$id ] + $fields, __METHOD__ ); if ( $usesTemp ) { $callback( $id ); } @@ -452,17 +482,18 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @param string $table * @param string $key * @param string $pk - * @param string $extraFields * @param string|Message $comment * @param array|null $data * @param array $expect */ public function testInsertRoundTrip_withKeyConstruction( - $table, $key, $pk, $extraFields, $comment, $data, $expect + $table, $key, $pk, $comment, $data, $expect ) { + static $id = 1000; + $expectOld = [ 'text' => $expect['text'], - 'message' => new RawMessage( '$1', [ $expect['text'] ] ), + 'message' => new RawMessage( '$1', [ Message::plaintextParam( $expect['text'] ) ] ), 'data' => null, ]; @@ -475,12 +506,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { ]; foreach ( $stages as $writeStage => $possibleReadStages ) { - if ( $key === 'ipb_reason' ) { - $extraFields['ipb_address'] = __CLASS__ . "#$writeStage"; - } - $wstore = $this->makeStoreWithKey( $writeStage, $key ); - $usesTemp = $key === 'rev_comment'; + $usesTemp = $key === 'cs2_comment'; if ( $usesTemp ) { list( $fields, $callback ) = $wstore->insertWithTempTable( @@ -501,8 +528,7 @@ class CommentStoreTest extends MediaWikiLangTestCase { $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" ); } - $this->db->insert( $table, $extraFields + $fields, __METHOD__ ); - $id = $this->db->insertId(); + $this->db->insert( $table, [ $pk => ++$id ] + $fields, __METHOD__ ); if ( $usesTemp ) { $callback( $id ); } @@ -545,62 +571,50 @@ class CommentStoreTest extends MediaWikiLangTestCase { $db = wfGetDB( DB_REPLICA ); // for timestamps $msgComment = new Message( 'parentheses', [ 'message comment' ] ); - $textCommentMsg = new RawMessage( '$1', [ 'text comment' ] ); + $textCommentMsg = new RawMessage( '$1', [ Message::plaintextParam( '{{text}} comment' ) ] ); $nestedMsgComment = new Message( [ 'parentheses', 'rawmessage' ], [ new Message( 'mainpage' ) ] ); - $ipbfields = [ - 'ipb_range_start' => '', - 'ipb_range_end' => '', - 'ipb_timestamp' => $db->timestamp(), - 'ipb_expiry' => $db->getInfinity(), - ]; - $revfields = [ - 'rev_page' => 42, - 'rev_text_id' => 42, - 'rev_len' => 0, - 'rev_timestamp' => $db->timestamp(), - ]; $comStoreComment = new CommentStoreComment( null, 'comment store comment', null, [ 'foo' => 'bar' ] ); return [ 'Simple table, text comment' => [ - 'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', null, [ - 'text' => 'text comment', + 'commentstore1', 'cs1_comment', 'cs1_id', '{{text}} comment', null, [ + 'text' => '{{text}} comment', 'message' => $textCommentMsg, 'data' => null, ] ], 'Simple table, text comment with data' => [ - 'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', [ 'message' => 42 ], [ - 'text' => 'text comment', + 'commentstore1', 'cs1_comment', 'cs1_id', '{{text}} comment', [ 'message' => 42 ], [ + 'text' => '{{text}} comment', 'message' => $textCommentMsg, 'data' => [ 'message' => 42 ], ] ], 'Simple table, message comment' => [ - 'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, null, [ + 'commentstore1', 'cs1_comment', 'cs1_id', $msgComment, null, [ 'text' => '(message comment)', 'message' => $msgComment, 'data' => null, ] ], 'Simple table, message comment with data' => [ - 'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, [ 'message' => 42 ], [ + 'commentstore1', 'cs1_comment', 'cs1_id', $msgComment, [ 'message' => 42 ], [ 'text' => '(message comment)', 'message' => $msgComment, 'data' => [ 'message' => 42 ], ] ], 'Simple table, nested message comment' => [ - 'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $nestedMsgComment, null, [ + 'commentstore1', 'cs1_comment', 'cs1_id', $nestedMsgComment, null, [ 'text' => '(Main Page)', 'message' => $nestedMsgComment, 'data' => null, ] ], 'Simple table, CommentStoreComment' => [ - 'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, clone $comStoreComment, [ 'baz' => 'baz' ], [ + 'commentstore1', 'cs1_comment', 'cs1_id', clone $comStoreComment, [ 'baz' => 'baz' ], [ 'text' => 'comment store comment', 'message' => $comStoreComment->message, 'data' => [ 'foo' => 'bar' ], @@ -608,42 +622,42 @@ class CommentStoreTest extends MediaWikiLangTestCase { ], 'Revision, text comment' => [ - 'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', null, [ - 'text' => 'text comment', + 'commentstore2', 'cs2_comment', 'cs2_id', '{{text}} comment', null, [ + 'text' => '{{text}} comment', 'message' => $textCommentMsg, 'data' => null, ] ], 'Revision, text comment with data' => [ - 'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', [ 'message' => 42 ], [ - 'text' => 'text comment', + 'commentstore2', 'cs2_comment', 'cs2_id', '{{text}} comment', [ 'message' => 42 ], [ + 'text' => '{{text}} comment', 'message' => $textCommentMsg, 'data' => [ 'message' => 42 ], ] ], 'Revision, message comment' => [ - 'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, null, [ + 'commentstore2', 'cs2_comment', 'cs2_id', $msgComment, null, [ 'text' => '(message comment)', 'message' => $msgComment, 'data' => null, ] ], 'Revision, message comment with data' => [ - 'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, [ 'message' => 42 ], [ + 'commentstore2', 'cs2_comment', 'cs2_id', $msgComment, [ 'message' => 42 ], [ 'text' => '(message comment)', 'message' => $msgComment, 'data' => [ 'message' => 42 ], ] ], 'Revision, nested message comment' => [ - 'revision', 'rev_comment', 'rev_id', $revfields, $nestedMsgComment, null, [ + 'commentstore2', 'cs2_comment', 'cs2_id', $nestedMsgComment, null, [ 'text' => '(Main Page)', 'message' => $nestedMsgComment, 'data' => null, ] ], 'Revision, CommentStoreComment' => [ - 'revision', 'rev_comment', 'rev_id', $revfields, clone $comStoreComment, [ 'baz' => 'baz' ], [ + 'commentstore2', 'cs2_comment', 'cs2_id', clone $comStoreComment, [ 'baz' => 'baz' ], [ 'text' => 'comment store comment', 'message' => $comStoreComment->message, 'data' => [ 'foo' => 'bar' ], diff --git a/tests/phpunit/includes/CommentStoreTest.sql b/tests/phpunit/includes/CommentStoreTest.sql new file mode 100644 index 0000000000..f95781dd3e --- /dev/null +++ b/tests/phpunit/includes/CommentStoreTest.sql @@ -0,0 +1,17 @@ +-- These are carefully crafted to work in all five supported databases + +CREATE TABLE /*_*/commentstore1 ( + cs1_id integer not null, + cs1_comment varchar(200), + cs1_comment_id integer +); + +CREATE TABLE /*_*/commentstore2 ( + cs2_id integer not null, + cs2_comment varchar(200) +); + +CREATE TABLE /*_*/commentstore2_temp ( + cs2t_id integer not null, + cs2t_comment_id integer +); diff --git a/tests/phpunit/includes/DiffHistoryBlobTest.php b/tests/phpunit/includes/DiffHistoryBlobTest.php index c267a30e05..b75862ea0c 100644 --- a/tests/phpunit/includes/DiffHistoryBlobTest.php +++ b/tests/phpunit/includes/DiffHistoryBlobTest.php @@ -15,7 +15,6 @@ class DiffHistoryBlobTest extends MediaWikiTestCase { } /** - * Test for DiffHistoryBlob::xdiffAdler32() * @dataProvider provideXdiffAdler32 * @covers DiffHistoryBlob::xdiffAdler32 */ diff --git a/tests/phpunit/includes/EditPageTest.php b/tests/phpunit/includes/EditPageTest.php index 55d8fbb444..f5fef61a0c 100644 --- a/tests/phpunit/includes/EditPageTest.php +++ b/tests/phpunit/includes/EditPageTest.php @@ -368,6 +368,9 @@ class EditPageTest extends MediaWikiLangTestCase { } } + /** + * @covers EditPage + */ public function testUpdatePage() { $checkIds = []; @@ -414,6 +417,9 @@ class EditPageTest extends MediaWikiLangTestCase { $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" ); } + /** + * @covers EditPage + */ public function testUpdatePageTrx() { $text = "one"; $edit = [ @@ -684,6 +690,7 @@ hello /** * @depends testAutoMerge + * @covers EditPage */ public function testCheckDirectEditingDisallowed_forNonTextContent() { $title = Title::newFromText( 'Dummy:NonTextPageForEditPage' ); diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php index eac56fb841..8085bc710c 100644 --- a/tests/phpunit/includes/FauxResponseTest.php +++ b/tests/phpunit/includes/FauxResponseTest.php @@ -1,7 +1,5 @@ options; - } -} +use Wikimedia\TestingAccessWrapper; /** * Test class for FormOptions initialization @@ -39,11 +22,11 @@ class FormOptionsInitializationTest extends MediaWikiTestCase { */ protected function setUp() { parent::setUp(); - $this->object = new FormOptionsExposed(); + $this->object = TestingAccessWrapper::newFromObject( new FormOptions() ); } /** - * @covers FormOptionsExposed::add + * @covers FormOptions::add */ public function testAddStringOption() { $this->object->add( 'foo', 'string value' ); @@ -56,12 +39,12 @@ class FormOptionsInitializationTest extends MediaWikiTestCase { 'value' => null, ] ], - $this->object->getOptions() + $this->object->options ); } /** - * @covers FormOptionsExposed::add + * @covers FormOptions::add */ public function testAddIntegers() { $this->object->add( 'one', 1 ); @@ -81,7 +64,7 @@ class FormOptionsInitializationTest extends MediaWikiTestCase { 'type' => FormOptions::INT, ] ], - $this->object->getOptions() + $this->object->options ); } } diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php index 1d687e517c..999e0bb56c 100644 --- a/tests/phpunit/includes/HtmlTest.php +++ b/tests/phpunit/includes/HtmlTest.php @@ -212,7 +212,6 @@ class HtmlTest extends MediaWikiTestCase { } /** - * Test for Html::expandAttributes() * Please note it output a string prefixed with a space! * @covers Html::expandAttributes */ diff --git a/tests/phpunit/includes/LinkerTest.php b/tests/phpunit/includes/LinkerTest.php index 34e5593cfc..438d3e7bca 100644 --- a/tests/phpunit/includes/LinkerTest.php +++ b/tests/phpunit/includes/LinkerTest.php @@ -1,6 +1,5 @@ markTestSkippedIfDbType( 'postgres' ); + + $context = RequestContext::getMain(); + $user = $context->getUser(); + $user->setOption( 'showrollbackconfirmation', $rollbackEnabled ); + + $pageData = $this->insertPage( 'Rollback_Test_Page' ); + $page = WikiPage::factory( $pageData['title'] ); + + $updater = $page->newPageUpdater( $user ); + $updater->setContent( \MediaWiki\Revision\SlotRecord::MAIN, + new TextContent( 'Technical Wishes 123!' ) + ); + $summary = CommentStoreComment::newUnsavedComment( 'Some comment!' ); + $updater->saveRevision( $summary ); + + $rollbackOutput = Linker::generateRollback( $page->getRevision(), $context ); + $modules = $context->getOutput()->getModules(); + + $this->assertEquals( $expectedModules, $modules ); + $this->assertContains( 'rollback 1 edit', $rollbackOutput ); + } + + public static function provideCasesForRollbackGeneration() { + return [ + [ + true, + [ 'mediawiki.page.rollback.confirmation' ] + + ], + [ + false, + [] + ] + ]; + } + public static function provideCasesForFormatLinksInComment() { // phpcs:disable Generic.Files.LineLength return [ diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php index 1b91a87fb7..c95b1eb432 100644 --- a/tests/phpunit/includes/MWNamespaceTest.php +++ b/tests/phpunit/includes/MWNamespaceTest.php @@ -248,6 +248,7 @@ class MWNamespaceTest extends MediaWikiTestCase { * @param bool $expected */ public function testCanTalk( $index, $expected ) { + $this->hideDeprecated( 'MWNamespace::canTalk' ); $actual = MWNamespace::canTalk( $index ); $this->assertSame( $actual, $expected, "NS $index" ); } diff --git a/tests/phpunit/includes/MediaWikiTest.php b/tests/phpunit/includes/MediaWikiTest.php index 916a6ebafc..77bbc07b34 100644 --- a/tests/phpunit/includes/MediaWikiTest.php +++ b/tests/phpunit/includes/MediaWikiTest.php @@ -187,6 +187,7 @@ class MediaWikiTest extends MediaWikiTestCase { /** * Test a post-send job can not set cookies (T191537). + * @coversNothing */ public function testPostSendJobDoesNotSetCookie() { // Prevent updates from running diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index d75c0e5410..5d77cebac8 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -400,6 +400,9 @@ class MessageTest extends MediaWikiLangTestCase { $this->assertSame( 'example &', $msg->escaped() ); } + /** + * @covers CoreTagHooks::html + */ public function testRawHtmlInMsg() { $this->setMwGlobals( 'wgRawHtml', true ); // We have to reset the core hook registration. diff --git a/tests/phpunit/includes/MovePageTest.php b/tests/phpunit/includes/MovePageTest.php index 607f4f7a56..1b2b159f0a 100644 --- a/tests/phpunit/includes/MovePageTest.php +++ b/tests/phpunit/includes/MovePageTest.php @@ -65,6 +65,7 @@ class MovePageTest extends MediaWikiTestCase { /** * Test for the move operation being aborted via the TitleMove hook + * @covers MovePage::move */ public function testMoveAbortedByTitleMoveHook() { $error = 'Preventing move operation with TitleMove hook.'; diff --git a/tests/phpunit/includes/MultiHttpClientTest.php b/tests/phpunit/includes/MultiHttpClientTest.php index 7073b71023..1c7e62d092 100644 --- a/tests/phpunit/includes/MultiHttpClientTest.php +++ b/tests/phpunit/includes/MultiHttpClientTest.php @@ -1,8 +1,6 @@ '; + const ATOM_RC_LINK = ''; + + const RSS_TEST_LINK = ''; + const ATOM_TEST_LINK = ''; + // @codingStandardsIgnoreEnd + // Ensure that we don't affect the global ResourceLoader state. protected function setUp() { parent::setUp(); @@ -51,6 +59,64 @@ class OutputPageTest extends MediaWikiTestCase { ]; } + private function setupFeedLinks( $feed, $types ) { + $outputPage = $this->newInstance( [ + 'AdvertisedFeedTypes' => $types, + 'Feed' => $feed, + 'OverrideSiteFeed' => false, + 'Script' => '/w', + 'Sitename' => false, + ] ); + $outputPage->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) ); + $this->setMwGlobals( [ + 'wgScript' => '/w/index.php', + ] ); + return $outputPage; + } + + private function assertFeedLinks( $outputPage, $message, $present, $non_present ) { + $links = $outputPage->getHeadLinksArray(); + foreach ( $present as $link ) { + $this->assertContains( $link, $links, $message ); + } + foreach ( $non_present as $link ) { + $this->assertNotContains( $link, $links, $message ); + } + } + + private function assertFeedUILinks( $outputPage, $ui_links ) { + if ( $ui_links ) { + $this->assertTrue( $outputPage->isSyndicated(), 'Syndication should be offered' ); + $this->assertGreaterThan( 0, count( $outputPage->getSyndicationLinks() ), + 'Some syndication links should be there' ); + } else { + $this->assertFalse( $outputPage->isSyndicated(), 'No syndication should be offered' ); + $this->assertEquals( 0, count( $outputPage->getSyndicationLinks() ), + 'No syndication links should be there' ); + } + } + + public static function provideFeedLinkData() { + return [ + [ + true, [ 'rss' ], 'Only RSS RC link should be offerred', + [ self::RSS_RC_LINK ], [ self::ATOM_RC_LINK ] + ], + [ + true, [ 'atom' ], 'Only Atom RC link should be offerred', + [ self::ATOM_RC_LINK ], [ self::RSS_RC_LINK ] + ], + [ + true, [], 'No RC feed formats should be offerred', + [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ] + ], + [ + false, [ 'atom' ], 'No RC feeds should be offerred', + [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ] + ], + ]; + } + /** * @covers OutputPage::setCopyrightUrl * @covers OutputPage::getHeadLinksArray @@ -65,6 +131,67 @@ class OutputPageTest extends MediaWikiTestCase { ); } + /** + * @dataProvider provideFeedLinkData + * @covers OutputPage::getHeadLinksArray + */ + public function testRecentChangesFeed( $feed, $advertised_feed_types, + $message, $present, $non_present ) { + $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types ); + $this->assertFeedLinks( $outputPage, $message, $present, $non_present ); + } + + public static function provideAdditionalFeedData() { + return [ + [ + true, [ 'atom' ], 'Additional Atom feed should be offered', + 'atom', + [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ], + [ self::RSS_TEST_LINK, self::RSS_RC_LINK ], + true, + ], + [ + true, [ 'rss' ], 'Additional RSS feed should be offered', + 'rss', + [ self::RSS_TEST_LINK, self::RSS_RC_LINK ], + [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ], + true, + ], + [ + true, [ 'rss' ], 'Additional Atom feed should NOT be offered with RSS enabled', + 'atom', + [ self::RSS_RC_LINK ], + [ self::RSS_TEST_LINK, self::ATOM_TEST_LINK, self::ATOM_RC_LINK ], + false, + ], + [ + false, [ 'atom' ], 'Additional Atom feed should NOT be offered, all feeds disabled', + 'atom', + [], + [ + self::RSS_TEST_LINK, self::ATOM_TEST_LINK, + self::ATOM_RC_LINK, self::ATOM_RC_LINK, + ], + false, + ], + ]; + } + + /** + * @dataProvider provideAdditionalFeedData + * @covers OutputPage::getHeadLinksArray + * @covers OutputPage::addFeedLink + * @covers OutputPage::getSyndicationLinks + * @covers OutputPage::isSyndicated + */ + public function testAdditionalFeeds( $feed, $advertised_feed_types, $message, + $additional_feed_type, $present, $non_present, $any_ui_links ) { + $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types ); + $outputPage->addFeedLink( $additional_feed_type, 'fake-link' ); + $this->assertFeedLinks( $outputPage, $message, $present, $non_present ); + $this->assertFeedUILinks( $outputPage, $any_ui_links ); + } + // @todo How to test setStatusCode? /** @@ -797,7 +924,7 @@ class OutputPageTest extends MediaWikiTestCase { * @covers OutputPage::isSyndicated */ public function testSetSyndicated() { - $op = $this->newInstance(); + $op = $this->newInstance( [ 'Feed' => true ] ); $this->assertFalse( $op->isSyndicated() ); $op->setSyndicated(); @@ -805,6 +932,12 @@ class OutputPageTest extends MediaWikiTestCase { $op->setSyndicated( false ); $this->assertFalse( $op->isSyndicated() ); + + $op = $this->newInstance(); // Feed => false by default + $this->assertFalse( $op->isSyndicated() ); + + $op->setSyndicated(); + $this->assertFalse( $op->isSyndicated() ); } /** @@ -814,7 +947,7 @@ class OutputPageTest extends MediaWikiTestCase { * @covers OutputPage::getSyndicationLinks() */ public function testFeedLinks() { - $op = $this->newInstance(); + $op = $this->newInstance( [ 'Feed' => true ] ); $this->assertSame( [], $op->getSyndicationLinks() ); $op->addFeedLink( 'not a supported format', 'abc' ); @@ -839,6 +972,13 @@ class OutputPageTest extends MediaWikiTestCase { $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" ); } $this->assertSame( $expected, $op->getSyndicationLinks() ); + + $op = $this->newInstance(); // Feed => false by default + $this->assertSame( [], $op->getSyndicationLinks() ); + + $op->addFeedLink( $feedTypes[0], 'def' ); + $this->assertFalse( $op->isSyndicated() ); + $this->assertSame( [], $op->getSyndicationLinks() ); } /** @@ -912,7 +1052,7 @@ class OutputPageTest extends MediaWikiTestCase { * @param array $args Array of form [ category name => sort key ] * @param array $fakeResults Array of form [ category name => value to return from mocked * LinkBatch ] - * @param callback $variantLinkCallback Callback to replace findVariantLink() call + * @param callable $variantLinkCallback Callback to replace findVariantLink() call * @param array $expectedNormal Expected return value of getCategoryLinks['normal'] * @param array $expectedHidden Expected return value of getCategoryLinks['hidden'] */ @@ -1486,7 +1626,7 @@ class OutputPageTest extends MediaWikiTestCase { "

Bold\n

", ], 'No section edit links' => [ [ '== Title ==' ], - "

Title

\n", + "

Title

", ], ], 'addWikiTextWithTitle' => [ @@ -1515,7 +1655,7 @@ class OutputPageTest extends MediaWikiTestCase { '

* Not a list

', ], 'No section edit links' => [ [ '== Title ==' ], - "

Title

\n", + "

Title

", ], 'With title at start' => [ [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ], "
  • Some page
\n", @@ -1531,10 +1671,10 @@ class OutputPageTest extends MediaWikiTestCase { // Preferred interface: output is tidied 'SpecialNewimages' => [ [ "

\nMy message" ], - '

' . "\nMy message\n

" + '

' . "\nMy message

" ], 'List at start' => [ [ '* List' ], - "
  • List
\n", + "
  • List
", ], 'List not at start' => [ [ '* Not a list', false ], '

* Not a list

', @@ -1546,7 +1686,7 @@ class OutputPageTest extends MediaWikiTestCase { "

* Some page

", ], 'EditPage' => [ [ "
{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ], - '
' . "Some page\n
" + '
' . "Some page
" ], ], 'wrapWikiTextAsInterface' => [ @@ -1555,7 +1695,7 @@ class OutputPageTest extends MediaWikiTestCase { "

text\n

" ], 'Spurious
' => [ [ 'wrapperClass', 'text
more' ], - "

text

more\n
" + "

text

more
" ], 'Extra newlines would break

wrappers' => [ [ 'two classes', "1\n\n2\n\n3" ], "

1\n

2\n

3\n

" @@ -1744,7 +1884,6 @@ class OutputPageTest extends MediaWikiTestCase { // @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests // for them: // * addModules() - // * addModuleScripts() // * addModuleStyles() // * addJsConfigVars() // * enableOOUI() @@ -1830,12 +1969,12 @@ class OutputPageTest extends MediaWikiTestCase { return [ 'List at start of line (content)' => [ [ '* List', true, false ], - "
  • List
\n
", - "
  • List
\n", + "
  • List
", + "
  • List
", ], 'List at start of line (interface)' => [ [ '* List', true, true ], - "
  • List
\n", + "
  • List
", ], 'List not at start (content)' => [ [ "* ''Not'' list", false, false ], @@ -1873,9 +2012,8 @@ class OutputPageTest extends MediaWikiTestCase { 'No section edit links' => [ [ '== Header ==' ], '

' . - "Header

\n
", - '

Header

' . - "\n", + "Header
", + '

Header

', ] ]; } @@ -1926,7 +2064,7 @@ class OutputPageTest extends MediaWikiTestCase { return [ 'List at start of line' => [ [ '* List', true ], - "
  • List
\n", + "
  • List
", ], 'List not at start' => [ [ "* ''Not'' list", false ], @@ -1945,8 +2083,7 @@ class OutputPageTest extends MediaWikiTestCase { ], 'No section edit links' => [ [ '== Header ==' ], - '

Header

' . - "\n", + '

Header

', ] ]; } @@ -2531,7 +2668,7 @@ class OutputPageTest extends MediaWikiTestCase { $nonce->setAccessible( true ); $nonce->setValue( $out, 'secret' ); $rl = $out->getResourceLoader(); - $rl->setMessageBlobStore( new NullMessageBlobStore() ); + $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) ); $rl->register( [ 'test.foo' => new ResourceLoaderTestModule( [ 'script' => 'mw.test.foo( { a: true } );', @@ -2575,14 +2712,14 @@ class OutputPageTest extends MediaWikiTestCase { [ [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ], "" ], // Multiple only=styles load [ [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ], - '' + '' ], // Private embed (only=scripts) [ @@ -2607,14 +2744,14 @@ class OutputPageTest extends MediaWikiTestCase { // noscript group [ [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ], - '' + '' ], // Load two modules in separate groups [ [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ], "" ], ]; @@ -2647,7 +2784,7 @@ class OutputPageTest extends MediaWikiTestCase { ->method( 'buildCssLinksArray' ) ->willReturn( [] ); $rl = $op->getResourceLoader(); - $rl->setMessageBlobStore( new NullMessageBlobStore() ); + $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) ); // Register custom modules $rl->register( [ @@ -2678,13 +2815,13 @@ class OutputPageTest extends MediaWikiTestCase { 'default logged-out' => [ 'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ], '' . "\n" . - '', + '', ], 'default logged-in' => [ 'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ], '' . "\n" . - '' . "\n" . - '', + '' . "\n" . + '', ], 'custom modules' => [ 'exemptStyleModules' => [ @@ -2692,10 +2829,10 @@ class OutputPageTest extends MediaWikiTestCase { 'user' => [ 'user.styles', 'example.user' ], ], '' . "\n" . - '' . "\n" . - '' . "\n" . - '' . "\n" . - '', + '' . "\n" . + '' . "\n" . + '' . "\n" . + '', ], ]; // phpcs:enable @@ -3051,21 +3188,3 @@ class OutputPageTest extends MediaWikiTestCase { return new OutputPage( $context ); } } - -/** - * MessageBlobStore that doesn't do anything - */ -class NullMessageBlobStore extends MessageBlobStore { - public function get( ResourceLoader $resourceLoader, $modules, $lang ) { - return []; - } - - public function updateModule( $name, ResourceLoaderModule $module, $lang ) { - } - - public function updateMessage( $key ) { - } - - public function clear() { - } -} diff --git a/tests/phpunit/includes/Permissions/PermissionManagerTest.php b/tests/phpunit/includes/Permissions/PermissionManagerTest.php new file mode 100644 index 0000000000..7f5ec40eb2 --- /dev/null +++ b/tests/phpunit/includes/Permissions/PermissionManagerTest.php @@ -0,0 +1,1410 @@ +'; + + protected function setUp() { + parent::setUp(); + + $localZone = 'UTC'; + $localOffset = date( 'Z' ) / 60; + + $this->setMwGlobals( [ + 'wgLocaltimezone' => $localZone, + 'wgLocalTZoffset' => $localOffset, + 'wgNamespaceProtection' => [ + NS_MEDIAWIKI => 'editinterface', + ], + ] ); + // Without this testUserBlock will use a non-English context on non-English MediaWiki + // installations (because of how Title::checkUserBlock is implemented) and fail. + RequestContext::resetMain(); + + $this->userName = 'Useruser'; + $this->altUserName = 'Altuseruser'; + date_default_timezone_set( $localZone ); + + $this->title = Title::makeTitle( NS_MAIN, "Main Page" ); + if ( !isset( $this->userUser ) || !( $this->userUser instanceof User ) ) { + $this->userUser = User::newFromName( $this->userName ); + + if ( !$this->userUser->getId() ) { + $this->userUser = User::createNew( $this->userName, [ + "email" => "test@example.com", + "real_name" => "Test User" ] ); + $this->userUser->load(); + } + + $this->altUser = User::newFromName( $this->altUserName ); + if ( !$this->altUser->getId() ) { + $this->altUser = User::createNew( $this->altUserName, [ + "email" => "alttest@example.com", + "real_name" => "Test User Alt" ] ); + $this->altUser->load(); + } + + $this->anonUser = User::newFromId( 0 ); + + $this->user = $this->userUser; + } + + $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + + $this->overrideMwServices(); + } + + protected function setUserPerm( $perm ) { + // Setting member variables is evil!!! + + if ( is_array( $perm ) ) { + $this->user->mRights = $perm; + } else { + $this->user->mRights = [ $perm ]; + } + } + + protected function setTitle( $ns, $title = "Main_Page" ) { + $this->title = Title::makeTitle( $ns, $title ); + } + + protected function setUser( $userName = null ) { + if ( $userName === 'anon' ) { + $this->user = $this->anonUser; + } elseif ( $userName === null || $userName === $this->userName ) { + $this->user = $this->userUser; + } else { + $this->user = $this->altUser; + } + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * + * This test is failing per T201776. + * + * @group Broken + * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions + */ + public function testQuickPermissions() { + $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> + getFormattedNsText( NS_PROJECT ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ "nocreatetext" ] ], $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ 'nocreatetext' ] ], $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ 'nocreatetext' ] ], $res ); + + $this->setUser( $this->userName ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ); + $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setUser( $this->userName ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ], $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenotallowed' ] ], $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ], $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenologintext' ] ], $res ); + + $this->setUser( $this->userName ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( + 'move', + [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ] + ); + + $this->setUser( 'anon' ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( + 'move', + [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ], + [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ] + ); + + if ( $this->isWikitextNS( NS_MAIN ) ) { + // NOTE: some content models don't allow moving + // @todo find a Wikitext namespace for testing + + $this->setTitle( NS_MAIN ); + $this->setUser( 'anon' ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', [] ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ], + [ [ 'movenologintext' ] ] ); + + $this->setUser( $this->userName ); + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ] ); + + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', [] ); + + $this->setUser( 'anon' ); + $this->setUserPerm( 'move' ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setUserPerm( '' ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [ [ 'movenotallowed' ] ], $res ); + } + + $this->setTitle( NS_USER ); + $this->setUser( $this->userName ); + $this->setUserPerm( [ "move", "move-rootuserpages" ] ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setUserPerm( "move" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [ [ 'cant-move-to-user-page' ] ], $res ); + + $this->setUser( 'anon' ); + $this->setUserPerm( [ "move", "move-rootuserpages" ] ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setTitle( NS_USER, "User/subpage" ); + $this->setUserPerm( [ "move", "move-rootuserpages" ] ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setUserPerm( "move" ); + $res = $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ); + $this->assertEquals( [], $res ); + + $this->setUser( 'anon' ); + $check = [ + 'edit' => [ + [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ] ], + [ [ 'badaccess-group0' ] ], + [], + true + ], + 'protect' => [ + [ [ + 'badaccess-groups', + "[[$prefix:Administrators|Administrators]]", 1 ], + [ 'protect-cantedit' + ] ], + [ [ 'badaccess-group0' ], [ 'protect-cantedit' ] ], + [ [ 'protect-cantedit' ] ], + false + ], + '' => [ [], [], [], true ] + ]; + + foreach ( [ "edit", "protect", "" ] as $action ) { + $this->setUserPerm( null ); + $this->assertEquals( $check[$action][0], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, true ) ); + $this->assertEquals( $check[$action][0], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) ); + $this->assertEquals( $check[$action][0], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) ); + + global $wgGroupPermissions; + $old = $wgGroupPermissions; + $wgGroupPermissions = []; + + $this->assertEquals( $check[$action][1], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, true ) ); + $this->assertEquals( $check[$action][1], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) ); + $this->assertEquals( $check[$action][1], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) ); + $wgGroupPermissions = $old; + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][2], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, true ) ); + $this->assertEquals( $check[$action][2], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) ); + $this->assertEquals( $check[$action][2], + $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) ); + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][3], + $this->permissionManager->userCan( $action, $this->user, $this->title, true ) ); + $this->assertEquals( $check[$action][3], + $this->permissionManager->userCan( $action, $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + # count( User::getGroupsWithPermissions( $action ) ) < 1 + } + } + + protected function runGroupPermissions( $action, $result, $result2 = null ) { + global $wgGroupPermissions; + + if ( $result2 === null ) { + $result2 = $result; + } + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = false; + $res = $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title ); + $this->assertEquals( $result, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = false; + $res = $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = true; + $res = $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = true; + $res = $this->permissionManager + ->getPermissionErrors( $action, $this->user, $this->title ); + $this->assertEquals( $result2, $res ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions + */ + public function testSpecialsAndNSPermissions() { + global $wgNamespaceProtection; + $this->setUser( $this->userName ); + + $this->setTitle( NS_SPECIAL ); + + $this->assertEquals( [ [ 'badaccess-group0' ], [ 'ns-specialprotected' ] ], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( '' ); + $this->assertEquals( [ [ 'badaccess-group0' ] ], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $wgNamespaceProtection[NS_USER] = [ 'bogus' ]; + + $this->setTitle( NS_USER ); + $this->setUserPerm( '' ); + $this->assertEquals( [ [ 'badaccess-group0' ], + [ 'namespaceprotected', 'User', 'bogus' ] ], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $wgNamespaceProtection = null; + + $this->setUserPerm( 'bogus' ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + $this->assertEquals( true, + $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) ); + + $this->setUserPerm( '' ); + $this->assertEquals( [ [ 'badaccess-group0' ] ], + $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testJsConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->userName . '/test.js' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testJsonConfigEditPermissions() { + $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> + getFormattedNsText( NS_PROJECT ); + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->userName . '/test.json' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testCssConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->userName . '/test.css' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testOtherJsConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.js' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testOtherJsonConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.json' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testOtherCssConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.css' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testOtherNonConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->altUserName . '/tempo' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + /** + * @todo This should use data providers like the other methods here. + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions + */ + public function testPatrolActionConfigEditPermissions() { + $this->setUser( 'anon' ); + $this->setTitle( NS_USER, 'ToPatrolOrNotToPatrol' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-groups' ] ] + ); + } + + protected function runConfigEditPermissions( + $resultNone, + $resultMyCss, + $resultMyJson, + $resultMyJs, + $resultUserCss, + $resultUserJson, + $resultUserJs, + $resultPatrol + ) { + $this->setUserPerm( '' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultNone, $result ); + + $this->setUserPerm( 'editmyusercss' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultMyCss, $result ); + + $this->setUserPerm( 'editmyuserjson' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultMyJson, $result ); + + $this->setUserPerm( 'editmyuserjs' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultMyJs, $result ); + + $this->setUserPerm( 'editusercss' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultUserCss, $result ); + + $this->setUserPerm( 'edituserjson' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultUserJson, $result ); + + $this->setUserPerm( 'edituserjs' ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( $resultUserJs, $result ); + + $this->setUserPerm( '' ); + $result = $this->permissionManager + ->getPermissionErrors( 'patrol', $this->user, $this->title ); + $this->assertEquals( reset( $resultPatrol[0] ), reset( $result[0] ) ); + + $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] ); + $result = $this->permissionManager + ->getPermissionErrors( 'bogus', $this->user, $this->title ); + $this->assertEquals( [ [ 'badaccess-group0' ] ], $result ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * + * This test is failing per T201776. + * + * @group Broken + * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions + */ + public function testPageRestrictions() { + $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> + getFormattedNsText( NS_PROJECT ); + + $this->setTitle( NS_MAIN ); + $this->title->mRestrictionsLoaded = true; + $this->setUserPerm( "edit" ); + $this->title->mRestrictions = [ "bogus" => [ 'bogus', "sysop", "protect", "" ] ]; + + $this->assertEquals( [], + $this->permissionManager->getPermissionErrors( 'edit', + $this->user, $this->title ) ); + + $this->assertEquals( true, + $this->permissionManager->userCan( 'edit', $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + + $this->title->mRestrictions = [ "edit" => [ 'bogus', "sysop", "protect", "" ], + "bogus" => [ 'bogus', "sysop", "protect", "" ] ]; + + $this->assertEquals( [ [ 'badaccess-group0' ], + [ 'protectedpagetext', 'bogus', 'bogus' ], + [ 'protectedpagetext', 'editprotected', 'bogus' ], + [ 'protectedpagetext', 'protect', 'bogus' ] ], + $this->permissionManager->getPermissionErrors( 'bogus', + $this->user, $this->title ) ); + $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ], + [ 'protectedpagetext', 'editprotected', 'edit' ], + [ 'protectedpagetext', 'protect', 'edit' ] ], + $this->permissionManager->getPermissionErrors( 'edit', + $this->user, $this->title ) ); + $this->setUserPerm( "" ); + $this->assertEquals( [ [ 'badaccess-group0' ], + [ 'protectedpagetext', 'bogus', 'bogus' ], + [ 'protectedpagetext', 'editprotected', 'bogus' ], + [ 'protectedpagetext', 'protect', 'bogus' ] ], + $this->permissionManager->getPermissionErrors( 'bogus', + $this->user, $this->title ) ); + $this->assertEquals( [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ], + [ 'protectedpagetext', 'bogus', 'edit' ], + [ 'protectedpagetext', 'editprotected', 'edit' ], + [ 'protectedpagetext', 'protect', 'edit' ] ], + $this->permissionManager->getPermissionErrors( 'edit', + $this->user, $this->title ) ); + $this->setUserPerm( [ "edit", "editprotected" ] ); + $this->assertEquals( [ [ 'badaccess-group0' ], + [ 'protectedpagetext', 'bogus', 'bogus' ], + [ 'protectedpagetext', 'protect', 'bogus' ] ], + $this->permissionManager->getPermissionErrors( 'bogus', + $this->user, $this->title ) ); + $this->assertEquals( [ + [ 'protectedpagetext', 'bogus', 'edit' ], + [ 'protectedpagetext', 'protect', 'edit' ] ], + $this->permissionManager->getPermissionErrors( 'edit', + $this->user, $this->title ) ); + + $this->title->mCascadeRestriction = true; + $this->setUserPerm( "edit" ); + + $this->assertEquals( false, + $this->permissionManager->userCan( 'bogus', $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + + $this->assertEquals( false, + $this->permissionManager->userCan( 'edit', $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + + $this->assertEquals( [ [ 'badaccess-group0' ], + [ 'protectedpagetext', 'bogus', 'bogus' ], + [ 'protectedpagetext', 'editprotected', 'bogus' ], + [ 'protectedpagetext', 'protect', 'bogus' ] ], + $this->permissionManager->getPermissionErrors( 'bogus', + $this->user, $this->title ) ); + $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ], + [ 'protectedpagetext', 'editprotected', 'edit' ], + [ 'protectedpagetext', 'protect', 'edit' ] ], + $this->permissionManager->getPermissionErrors( 'edit', + $this->user, $this->title ) ); + + $this->setUserPerm( [ "edit", "editprotected" ] ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'bogus', $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + + $this->assertEquals( false, + $this->permissionManager->userCan( 'edit', $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + + $this->assertEquals( [ [ 'badaccess-group0' ], + [ 'protectedpagetext', 'bogus', 'bogus' ], + [ 'protectedpagetext', 'protect', 'bogus' ], + [ 'protectedpagetext', 'protect', 'bogus' ] ], + $this->permissionManager->getPermissionErrors( 'bogus', + $this->user, $this->title ) ); + $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ], + [ 'protectedpagetext', 'protect', 'edit' ], + [ 'protectedpagetext', 'protect', 'edit' ] ], + $this->permissionManager->getPermissionErrors( 'edit', + $this->user, $this->title ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions + */ + public function testCascadingSourcesRestrictions() { + $this->setTitle( NS_MAIN, "test page" ); + $this->setUserPerm( [ "edit", "bogus" ] ); + + $this->title->mCascadeSources = [ + Title::makeTitle( NS_MAIN, "Bogus" ), + Title::makeTitle( NS_MAIN, "UnBogus" ) + ]; + $this->title->mCascadingRestrictions = [ + "bogus" => [ 'bogus', "sysop", "protect", "" ] + ]; + + $this->assertEquals( false, + $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) ); + $this->assertEquals( [ + [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ], + [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ], + [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ] ], + $this->permissionManager->getPermissionErrors( 'bogus', $this->user, $this->title ) ); + + $this->assertEquals( true, + $this->permissionManager->userCan( 'edit', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager->getPermissionErrors( 'edit', $this->user, $this->title ) ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions + */ + public function testActionPermissions() { + $this->setUserPerm( [ "createpage" ] ); + $this->setTitle( NS_MAIN, "test page" ); + $this->title->mTitleProtection['permission'] = ''; + $this->title->mTitleProtection['user'] = $this->user->getId(); + $this->title->mTitleProtection['expiry'] = 'infinity'; + $this->title->mTitleProtection['reason'] = 'test'; + $this->title->mCascadeRestriction = false; + + $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ], + $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ) ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'create', $this->user, $this->title ) ); + + $this->title->mTitleProtection['permission'] = 'editprotected'; + $this->setUserPerm( [ 'createpage', 'protect' ] ); + $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ], + $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ) ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'create', $this->user, $this->title ) ); + + $this->setUserPerm( [ 'createpage', 'editprotected' ] ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ) ); + $this->assertEquals( true, + $this->permissionManager->userCan( 'create', $this->user, $this->title ) ); + + $this->setUserPerm( [ 'createpage' ] ); + $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ], + $this->permissionManager + ->getPermissionErrors( 'create', $this->user, $this->title ) ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'create', $this->user, $this->title ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->setUserPerm( [ "move" ] ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'move', $this->user, $this->title ) ); + $this->assertEquals( [ [ 'immobile-source-namespace', 'Media' ] ], + $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ) ); + + $this->setTitle( NS_HELP, "test page" ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ) ); + $this->assertEquals( true, + $this->permissionManager->userCan( 'move', $this->user, $this->title ) ); + + $this->title->mInterwiki = "no"; + $this->assertEquals( [ [ 'immobile-source-page' ] ], + $this->permissionManager + ->getPermissionErrors( 'move', $this->user, $this->title ) ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'move', $this->user, $this->title ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) ); + $this->assertEquals( [ [ 'immobile-target-namespace', 'Media' ] ], + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + + $this->setTitle( NS_HELP, "test page" ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + $this->assertEquals( true, + $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) ); + + $this->title->mInterwiki = "no"; + $this->assertEquals( [ [ 'immobile-target-page' ] ], + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + $this->assertEquals( false, + $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock + */ + public function testUserBlock() { + $this->setMwGlobals( [ + 'wgEmailConfirmToEdit' => true, + 'wgEmailAuthentication' => true, + ] ); + + $this->overrideMwServices(); + $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + + $this->setUserPerm( [ + 'createpage', + 'edit', + 'move', + 'rollback', + 'patrol', + 'upload', + 'purge' + ] ); + $this->setTitle( NS_HELP, "test page" ); + + # $wgEmailConfirmToEdit only applies to 'edit' action + $this->assertEquals( [], + $this->permissionManager->getPermissionErrors( 'move-target', + $this->user, $this->title ) ); + $this->assertContains( [ 'confirmedittext' ], + $this->permissionManager + ->getPermissionErrors( 'edit', $this->user, $this->title ) ); + + $this->setMwGlobals( 'wgEmailConfirmToEdit', false ); + $this->overrideMwServices(); + $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + + $this->assertNotContains( [ 'confirmedittext' ], + $this->permissionManager + ->getPermissionErrors( 'edit', $this->user, $this->title ) ); + + # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' + $this->assertEquals( [], + $this->permissionManager->getPermissionErrors( 'move-target', + $this->user, $this->title ) ); + + global $wgLang; + $prev = time(); + $now = time() + 120; + $this->user->mBlockedby = $this->user->getId(); + $this->user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $this->user->getId(), + 'reason' => 'no reason given', + 'timestamp' => $prev + 3600, + 'auto' => true, + 'expiry' => 0 + ] ); + $this->user->mBlock->mTimestamp = 0; + $this->assertEquals( [ [ 'autoblockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, 'infinite', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ] ], + $this->permissionManager->getPermissionErrors( 'move-target', + $this->user, $this->title ) ); + + $this->assertEquals( false, $this->permissionManager + ->userCan( 'move-target', $this->user, $this->title ) ); + // quickUserCan should ignore user blocks + $this->assertEquals( true, $this->permissionManager + ->userCan( 'move-target', $this->user, $this->title, + PermissionManager::RIGOR_QUICK ) ); + + global $wgLocalTZoffset; + $wgLocalTZoffset = -60; + $this->user->mBlockedby = $this->user->getName(); + $this->user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $this->user->getId(), + 'reason' => 'no reason given', + 'timestamp' => $now, + 'auto' => false, + 'expiry' => 10, + ] ); + $this->assertEquals( [ [ 'blockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ], + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) + # $user->blockedFor() == '' + # $user->mBlock->mExpiry == 'infinity' + + $this->user->mBlockedby = $this->user->getName(); + $this->user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $this->user->getId(), + 'reason' => 'no reason given', + 'timestamp' => $now, + 'auto' => false, + 'expiry' => 10, + 'systemBlock' => 'test', + ] ); + + $errors = [ [ 'systemblockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', 'test', '23:00, 31 December 1969', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ]; + + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'edit', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'rollback', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'patrol', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'upload', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'purge', $this->user, $this->title ) ); + + // partial block message test + $this->user->mBlockedby = $this->user->getName(); + $this->user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $this->user->getId(), + 'reason' => 'no reason given', + 'timestamp' => $now, + 'sitewide' => false, + 'expiry' => 10, + ] ); + + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'edit', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'rollback', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'patrol', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'upload', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'purge', $this->user, $this->title ) ); + + $this->user->mBlock->setRestrictions( [ + ( new PageRestriction( 0, $this->title->getArticleID() ) )->setTitle( $this->title ), + ] ); + + $errors = [ [ 'blockedtext-partial', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ]; + + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'edit', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'move-target', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'rollback', $this->user, $this->title ) ); + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'patrol', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'upload', $this->user, $this->title ) ); + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'purge', $this->user, $this->title ) ); + + // Test no block. + $this->user->mBlockedby = null; + $this->user->mBlock = null; + + $this->assertEquals( [], + $this->permissionManager + ->getPermissionErrors( 'edit', $this->user, $this->title ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock + * + * Tests to determine that the passed in permission does not get mixed up with + * an action of the same name. + */ + public function testUserBlockAction() { + global $wgLang; + + $tester = $this->getMockBuilder( Action::class ) + ->disableOriginalConstructor() + ->getMock(); + $tester->method( 'getName' ) + ->willReturn( 'tester' ); + $tester->method( 'getRestriction' ) + ->willReturn( 'test' ); + $tester->method( 'requiresUnblock' ) + ->willReturn( false ); + + $this->setMwGlobals( [ + 'wgActions' => [ + 'tester' => $tester, + ], + 'wgGroupPermissions' => [ + '*' => [ + 'tester' => true, + ], + ], + ] ); + + $now = time(); + $this->user->mBlockedby = $this->user->getName(); + $this->user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $this->user->getId(), + 'reason' => 'no reason given', + 'timestamp' => $now, + 'auto' => false, + 'expiry' => 'infinity', + ] ); + + $errors = [ [ 'blockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, 'infinite', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ]; + + $this->assertEquals( $errors, + $this->permissionManager + ->getPermissionErrors( 'tester', $this->user, $this->title ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom + */ + public function testBlockInstanceCache() { + // First, check the user isn't blocked + $user = $this->getMutableTestUser()->getUser(); + $ut = Title::makeTitle( NS_USER_TALK, $user->getName() ); + $this->assertNull( $user->getBlock( false ), 'sanity check' ); + //$this->assertSame( '', $user->blockedBy(), 'sanity check' ); + //$this->assertSame( '', $user->blockedFor(), 'sanity check' ); + //$this->assertFalse( (bool)$user->isHidden(), 'sanity check' ); + $this->assertFalse( $this->permissionManager + ->isBlockedFrom( $user, $ut ), 'sanity check' ); + + // Block the user + $blocker = $this->getTestSysop()->getUser(); + $block = new Block( [ + 'hideName' => true, + 'allowUsertalk' => false, + 'reason' => 'Because', + ] ); + $block->setTarget( $user ); + $block->setBlocker( $blocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'sanity check: Failed to insert block' ); + + // Clear cache and confirm it loaded the block properly + $user->clearInstanceCache(); + $this->assertInstanceOf( Block::class, $user->getBlock( false ) ); + //$this->assertSame( $blocker->getName(), $user->blockedBy() ); + //$this->assertSame( 'Because', $user->blockedFor() ); + //$this->assertTrue( (bool)$user->isHidden() ); + $this->assertTrue( $this->permissionManager->isBlockedFrom( $user, $ut ) ); + + // Unblock + $block->delete(); + + // Clear cache and confirm it loaded the not-blocked properly + $user->clearInstanceCache(); + $this->assertNull( $user->getBlock( false ) ); + //$this->assertSame( '', $user->blockedBy() ); + //$this->assertSame( '', $user->blockedFor() ); + //$this->assertFalse( (bool)$user->isHidden() ); + $this->assertFalse( $this->permissionManager->isBlockedFrom( $user, $ut ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom + * @dataProvider provideIsBlockedFrom + * @param string|null $title Title to test. + * @param bool $expect Expected result from User::isBlockedFrom() + * @param array $options Additional test options: + * - 'blockAllowsUTEdit': (bool, default true) Value for $wgBlockAllowsUTEdit + * - 'allowUsertalk': (bool, default false) Passed to Block::__construct() + * - 'pageRestrictions': (array|null) If non-empty, page restriction titles for the block. + */ + public function testIsBlockedFrom( $title, $expect, array $options = [] ) { + $this->setMwGlobals( [ + 'wgBlockAllowsUTEdit' => $options['blockAllowsUTEdit'] ?? true, + ] ); + + $user = $this->getTestUser()->getUser(); + + if ( $title === self::USER_TALK_PAGE ) { + $title = $user->getTalkPage(); + } else { + $title = Title::newFromText( $title ); + } + + $restrictions = []; + foreach ( $options['pageRestrictions'] ?? [] as $pagestr ) { + $page = $this->getExistingTestPage( + $pagestr === self::USER_TALK_PAGE ? $user->getTalkPage() : $pagestr + ); + $restrictions[] = new PageRestriction( 0, $page->getId() ); + } + foreach ( $options['namespaceRestrictions'] ?? [] as $ns ) { + $restrictions[] = new NamespaceRestriction( 0, $ns ); + } + + $block = new Block( [ + 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ), + 'allowUsertalk' => $options['allowUsertalk'] ?? false, + 'sitewide' => !$restrictions, + ] ); + $block->setTarget( $user ); + $block->setBlocker( $this->getTestSysop()->getUser() ); + if ( $restrictions ) { + $block->setRestrictions( $restrictions ); + } + $block->insert(); + + try { + $this->assertSame( $expect, $this->permissionManager->isBlockedFrom( $user, $title ) ); + } finally { + $block->delete(); + } + } + + public static function provideIsBlockedFrom() { + return [ + 'Sitewide block, basic operation' => [ 'Test page', true ], + 'Sitewide block, not allowing user talk' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => false, + ] + ], + 'Sitewide block, allowing user talk' => [ + self::USER_TALK_PAGE, false, [ + 'allowUsertalk' => true, + ] + ], + 'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => true, + 'blockAllowsUTEdit' => false, + ] + ], + 'Partial block, blocking the page' => [ + 'Test page', true, [ + 'pageRestrictions' => [ 'Test page' ], + ] + ], + 'Partial block, not blocking the page' => [ + 'Test page 2', false, [ + 'pageRestrictions' => [ 'Test page' ], + ] + ], + 'Partial block, not allowing user talk but user talk page is not blocked' => [ + self::USER_TALK_PAGE, false, [ + 'allowUsertalk' => false, + 'pageRestrictions' => [ 'Test page' ], + ] + ], + 'Partial block, allowing user talk but user talk page is blocked' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => true, + 'pageRestrictions' => [ self::USER_TALK_PAGE ], + ] + ], + 'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [ + self::USER_TALK_PAGE, false, [ + 'allowUsertalk' => false, + 'pageRestrictions' => [ 'Test page' ], + 'blockAllowsUTEdit' => false, + ] + ], + 'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => true, + 'pageRestrictions' => [ self::USER_TALK_PAGE ], + 'blockAllowsUTEdit' => false, + ] + ], + 'Partial user talk namespace block, not allowing user talk' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => false, + 'namespaceRestrictions' => [ NS_USER_TALK ], + ] + ], + 'Partial user talk namespace block, allowing user talk' => [ + self::USER_TALK_PAGE, false, [ + 'allowUsertalk' => true, + 'namespaceRestrictions' => [ NS_USER_TALK ], + ] + ], + 'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => true, + 'namespaceRestrictions' => [ NS_USER_TALK ], + 'blockAllowsUTEdit' => false, + ] + ], + ]; + } + +} diff --git a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php index f2f3da8ac5..5e32574d40 100644 --- a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php +++ b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php @@ -47,7 +47,7 @@ class MainSlotRoleHandlerTest extends MediaWikiTestCase { public function testFetDefaultModel() { $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] ); - // For the main handler, the namespace determins the defualt model + // For the main handler, the namespace determins the default model $titleMain = $this->makeTitleObject( NS_MAIN ); $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) ); diff --git a/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php index 3efd372e07..8bf8606788 100644 --- a/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php @@ -55,7 +55,7 @@ class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase { [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ], [ [ 1 ] ], [], - [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ] + [ 'text' => [ 'JOIN', [ 'rev_text_id = old_id' ] ] ] ); parent::assertRevisionExistsInDatabase( $rev ); diff --git a/tests/phpunit/includes/Revision/McrSchemaOverride.php b/tests/phpunit/includes/Revision/McrSchemaOverride.php index dbd271a40d..01105315cc 100644 --- a/tests/phpunit/includes/Revision/McrSchemaOverride.php +++ b/tests/phpunit/includes/Revision/McrSchemaOverride.php @@ -45,7 +45,7 @@ trait McrSchemaOverride { $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' ); $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' ); $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' ); - $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' ); + $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' ); } if ( !$this->hasPreMcrFields( $db ) ) { diff --git a/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php index 0385708802..68d30009a4 100644 --- a/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php @@ -55,7 +55,7 @@ class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase { [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ], [ [ 1 ] ], [], - [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ] + [ 'text' => [ 'JOIN', [ 'rev_text_id = old_id' ] ] ] ); parent::assertRevisionExistsInDatabase( $rev ); diff --git a/tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php b/tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php index 7279e64a6e..0e565e5133 100644 --- a/tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php +++ b/tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php @@ -31,7 +31,7 @@ class MutableRevisionSlotsTest extends RevisionSlotsTest { /** * @dataProvider provideConstructorFailue - * @param $slots + * @param array $slots * * @covers \MediaWiki\Revision\RevisionSlots::__construct * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal diff --git a/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php index 59481f087b..b9b7ad9270 100644 --- a/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php @@ -80,7 +80,7 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase { ] ), 'joins' => [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ], ], ] ]; @@ -115,7 +115,7 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase { ] ), 'joins' => [ - 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + 'text' => [ 'JOIN', [ 'rev_text_id=old_id' ] ], ], ] ]; diff --git a/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php index 43453353bd..b3c34c8281 100644 --- a/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php @@ -38,7 +38,7 @@ class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase { [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ], [ [ 1 ] ], [], - [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ] + [ 'text' => [ 'JOIN', [ 'rev_text_id = old_id' ] ] ] ); parent::assertRevisionExistsInDatabase( $rev ); diff --git a/tests/phpunit/includes/Revision/RenderedRevisionTest.php b/tests/phpunit/includes/Revision/RenderedRevisionTest.php index 43fccee194..96658678ba 100644 --- a/tests/phpunit/includes/Revision/RenderedRevisionTest.php +++ b/tests/phpunit/includes/Revision/RenderedRevisionTest.php @@ -78,8 +78,8 @@ class RenderedRevisionTest extends MediaWikiTestCase { } /** - * @param $articleId - * @param $revisionId + * @param int $articleId + * @param int $revisionId * @return Title */ private function getMockTitle( $articleId, $revisionId ) { diff --git a/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php b/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php index 7188cf50a7..3ee61f7494 100644 --- a/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php +++ b/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php @@ -52,23 +52,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { return $fields; } - protected function getOldCommentQueryFields( $prefix ) { - return [ - "{$prefix}_comment_text" => "{$prefix}_comment", - "{$prefix}_comment_data" => 'NULL', - "{$prefix}_comment_cid" => 'NULL', - ]; - } - - protected function getCompatCommentQueryFields( $prefix ) { - return [ - "{$prefix}_comment_text" - => "COALESCE( comment_{$prefix}_comment.comment_text, {$prefix}_comment )", - "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data", - "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id", - ]; - } - protected function getNewCommentQueryFields( $prefix ) { return [ "{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text", @@ -106,19 +89,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { ]; } - protected function getCompatCommentJoins( $prefix ) { - return [ - "temp_{$prefix}_comment" => [ - "LEFT JOIN", - "temp_{$prefix}_comment.revcomment_{$prefix} = {$prefix}_id", - ], - "comment_{$prefix}_comment" => [ - "LEFT JOIN", - "comment_{$prefix}_comment.comment_id = temp_{$prefix}_comment.revcomment_comment_id", - ], - ]; - } - protected function getTextQueryFields() { return [ 'old_text', @@ -154,7 +124,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { yield 'MCR, comment, actor' => [ [ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW, ], [ @@ -180,7 +149,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW, ], [ @@ -192,11 +160,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'fields' => array_merge( $this->getArchiveQueryFields( false ), $this->getNewActorQueryFields( 'ar' ), - $this->getCompatCommentQueryFields( 'ar' ) + $this->getNewCommentQueryFields( 'ar' ) ), 'joins' => [ 'comment_ar_comment' - => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ], + => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ], 'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ], ], ] @@ -206,7 +174,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, ], [ @@ -218,11 +185,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getArchiveQueryFields( true ), $this->getContentHandlerQueryFields( 'ar' ), $this->getOldActorQueryFields( 'ar' ), - $this->getCompatCommentQueryFields( 'ar' ) + $this->getNewCommentQueryFields( 'ar' ) ), 'joins' => [ 'comment_ar_comment' - => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ], + => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ], ], ] ]; @@ -230,19 +197,22 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => false, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [ 'tables' => [ 'archive', + 'comment_ar_comment' => 'comment', ], 'fields' => array_merge( $this->getArchiveQueryFields( true ), $this->getOldActorQueryFields( 'ar' ), - $this->getOldCommentQueryFields( 'ar' ) + $this->getNewCommentQueryFields( 'ar' ) ), - 'joins' => [], + 'joins' => [ + 'comment_ar_comment' + => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ], + ], ] ]; } @@ -253,7 +223,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW, ], [ 'page', 'user' ], @@ -275,7 +244,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ], 'user' => [ 'LEFT JOIN', [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ], @@ -298,7 +267,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, ], [ 'page', 'user' ], @@ -317,11 +285,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getPageQueryFields(), $this->getUserQueryFields(), $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ), - $this->getCompatCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => array_merge( [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ], 'user' => [ 'LEFT JOIN', [ @@ -329,9 +297,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'user_id = actor_rev_user.actor_user', ] ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], - $this->getNewActorJoins( 'rev' ), - $this->getCompatCommentJoins( 'rev' ) + $this->getNewActorJoins( 'rev' ) ), ] ]; @@ -340,7 +310,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, ], [ 'page', 'user' ], @@ -359,11 +328,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getPageQueryFields(), $this->getUserQueryFields(), $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ), - $this->getCompatCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => array_merge( [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ], 'user' => [ 'LEFT JOIN', [ @@ -371,9 +340,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'user_id = actor_rev_user.actor_user' ] ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], - $this->getNewActorJoins( 'rev' ), - $this->getCompatCommentJoins( 'rev' ) + $this->getNewActorJoins( 'rev' ) ), ] ]; @@ -382,7 +353,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, ], [], @@ -396,11 +366,13 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getRevisionQueryFields( true ), $this->getContentHandlerQueryFields( 'rev' ), $this->getOldActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ), - $this->getCompatCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), - 'joins' => array_merge( - $this->getCompatCommentJoins( 'rev' ) - ), + 'joins' => [ + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], + ], ] ]; yield 'MCR write-both/read-old, page, user' => [ @@ -408,7 +380,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, ], [ 'page', 'user' ], @@ -426,11 +397,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getUserQueryFields(), $this->getPageQueryFields(), $this->getOldActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ), - $this->getCompatCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => array_merge( [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ], 'user' => [ 'LEFT JOIN', [ @@ -438,8 +409,10 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'user_id = rev_user' ] ], - ], - $this->getCompatCommentJoins( 'rev' ) + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], + ] ), ] ]; @@ -447,42 +420,55 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [], [ - 'tables' => [ 'revision' ], + 'tables' => [ + 'revision', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', + ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), $this->getContentHandlerQueryFields( 'rev' ), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), - 'joins' => [], + 'joins' => [ + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], + ], ] ]; yield 'pre-MCR, page, user' => [ [ 'wgContentHandlerUseDB' => true, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [ 'page', 'user' ], [ - 'tables' => [ 'revision', 'page', 'user' ], + 'tables' => [ + 'revision', 'page', 'user', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', + ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), $this->getContentHandlerQueryFields( 'rev' ), $this->getPageQueryFields(), $this->getUserQueryFields(), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ], 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], ] ]; @@ -490,38 +476,51 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => false, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [], [ - 'tables' => [ 'revision' ], + 'tables' => [ + 'revision', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', + ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), - 'joins' => [], + 'joins' => [ + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], + ], ], ]; yield 'pre-MCR, no model, page' => [ [ 'wgContentHandlerUseDB' => false, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [ 'page' ], [ - 'tables' => [ 'revision', 'page' ], + 'tables' => [ + 'revision', 'page', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', + ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), $this->getPageQueryFields(), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => [ - 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ], + 'page' => [ 'JOIN', [ 'page_id = rev_page' ], ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], ], ]; @@ -529,20 +528,26 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => false, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [ 'user' ], [ - 'tables' => [ 'revision', 'user' ], + 'tables' => [ + 'revision', 'user', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', + ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), $this->getUserQueryFields(), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => [ 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], ], ]; @@ -550,20 +555,26 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => false, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [ 'text' ], [ - 'tables' => [ 'revision', 'text' ], + 'tables' => [ + 'revision', 'text', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', + ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), $this->getTextQueryFields(), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => [ - 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + 'text' => [ 'JOIN', [ 'rev_text_id=old_id' ] ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], ], ]; @@ -571,13 +582,14 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'wgContentHandlerUseDB' => false, 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], [ 'text', 'page', 'user' ], [ 'tables' => [ - 'revision', 'page', 'user', 'text' + 'revision', 'page', 'user', 'text', + 'temp_rev_comment' => 'revision_comment_temp', + 'comment_rev_comment' => 'comment', ], 'fields' => array_merge( $this->getRevisionQueryFields( true ), @@ -585,11 +597,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { $this->getUserQueryFields(), $this->getTextQueryFields(), $this->getOldActorQueryFields( 'rev' ), - $this->getOldCommentQueryFields( 'rev' ) + $this->getNewCommentQueryFields( 'rev' ) ), 'joins' => [ 'page' => [ - 'INNER JOIN', + 'JOIN', [ 'page_id = rev_page' ], ], 'user' => [ @@ -600,9 +612,12 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { ], ], 'text' => [ - 'INNER JOIN', + 'JOIN', [ 'rev_text_id=old_id' ], ], + 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ], + 'comment_rev_comment' + => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ], ], ], ]; @@ -671,7 +686,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'content_model', ], 'joins' => [ - 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ], + 'content' => [ 'JOIN', [ 'slot_content_id = content_id' ] ], ], ] ]; @@ -699,7 +714,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'model_name', ], 'joins' => [ - 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ], + 'content' => [ 'JOIN', [ 'slot_content_id = content_id' ] ], 'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ], ], ] @@ -833,7 +848,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { yield 'with model, comment, and actor' => [ [ 'wgContentHandlerUseDB' => true, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, ], 'fields' => array_merge( @@ -853,7 +867,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { ], $this->getContentHandlerQueryFields( 'rev' ), [ - 'rev_comment_old' => 'rev_comment', 'rev_comment_pk' => 'rev_id', ] ), @@ -861,7 +874,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { yield 'no mode, no comment, no actor' => [ [ 'wgContentHandlerUseDB' => false, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], 'fields' => array_merge( @@ -878,8 +890,8 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'rev_len', 'rev_parent_id', 'rev_sha1', - ], - $this->getOldCommentQueryFields( 'rev' ) + 'rev_comment_pk' => 'rev_id', + ] ), ]; } @@ -888,7 +900,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { yield 'with model, comment, and actor' => [ [ 'wgContentHandlerUseDB' => true, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, ], 'fields' => array_merge( @@ -909,7 +920,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { ], $this->getContentHandlerQueryFields( 'ar' ), [ - 'ar_comment_old' => 'ar_comment', 'ar_comment_id' => 'ar_comment_id', ] ), @@ -917,7 +927,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { yield 'no mode, no comment, no actor' => [ [ 'wgContentHandlerUseDB' => false, - 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ], 'fields' => array_merge( @@ -935,8 +944,8 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { 'ar_len', 'ar_parent_id', 'ar_sha1', - ], - $this->getOldCommentQueryFields( 'ar' ) + 'ar_comment_id' => 'ar_comment_id', + ] ), ]; } @@ -984,7 +993,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { public function testRevisionPageJoinCond() { $this->hideDeprecated( 'Revision::pageJoinCond' ); $this->assertEquals( - [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + [ 'JOIN', [ 'page_id = rev_page' ] ], Revision::pageJoinCond() ); } diff --git a/tests/phpunit/includes/Revision/RevisionRecordTests.php b/tests/phpunit/includes/Revision/RevisionRecordTests.php index a53ceccd83..5448320e66 100644 --- a/tests/phpunit/includes/Revision/RevisionRecordTests.php +++ b/tests/phpunit/includes/Revision/RevisionRecordTests.php @@ -1,6 +1,6 @@ [ diff --git a/tests/phpunit/includes/Revision/RevisionRendererTest.php b/tests/phpunit/includes/Revision/RevisionRendererTest.php index 59b5a2ce98..071ea68347 100644 --- a/tests/phpunit/includes/Revision/RevisionRendererTest.php +++ b/tests/phpunit/includes/Revision/RevisionRendererTest.php @@ -30,8 +30,8 @@ use WikitextContent; class RevisionRendererTest extends MediaWikiTestCase { /** - * @param $articleId - * @param $revisionId + * @param int $articleId + * @param int $revisionId * @return Title */ private function getMockTitle( $articleId, $revisionId ) { diff --git a/tests/phpunit/includes/Revision/RevisionSlotsTest.php b/tests/phpunit/includes/Revision/RevisionSlotsTest.php index d8e7d92b58..52593ecc42 100644 --- a/tests/phpunit/includes/Revision/RevisionSlotsTest.php +++ b/tests/phpunit/includes/Revision/RevisionSlotsTest.php @@ -31,7 +31,7 @@ class RevisionSlotsTest extends MediaWikiTestCase { /** * @dataProvider provideConstructorFailue - * @param $slots + * @param array $slots * * @covers \MediaWiki\Revision\RevisionSlots::__construct * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal diff --git a/tests/phpunit/includes/Revision/RevisionStoreCacheRecordTest.php b/tests/phpunit/includes/Revision/RevisionStoreCacheRecordTest.php new file mode 100644 index 0000000000..8684cd392b --- /dev/null +++ b/tests/phpunit/includes/Revision/RevisionStoreCacheRecordTest.php @@ -0,0 +1,89 @@ +resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $row = [ + 'rev_id' => '7', + 'rev_page' => strval( $title->getArticleID() ), + 'rev_timestamp' => '20200101000000', + 'rev_deleted' => 0, + 'rev_minor_edit' => 0, + 'rev_parent_id' => '5', + 'rev_len' => $slots->computeSize(), + 'rev_sha1' => $slots->computeSha1(), + 'rev_user' => '11', + 'page_latest' => '18', + ]; + + $row = array_merge( $row, $rowOverrides ); + + if ( !$callback ) { + $callback = function ( $revId ) use ( $row ) { + return (object)$row; + }; + } + + return new RevisionStoreCacheRecord( + $callback, + $title, + $user, + $comment, + (object)$row, + $slots + ); + } + + public function testCallback() { + // Provide a callback that returns non-default values. Asserting the revision returns + // these values confirms callback execution and behavior. Also confirm the callback + // is only invoked once, even for multiple getter calls. + $rowOverrides = [ + 'rev_deleted' => RevisionRecord::DELETED_TEXT, + 'rev_user' => 12, + ]; + $callbackInvoked = 0; + $callback = function ( $revId ) use ( &$callbackInvoked, $rowOverrides ) { + $callbackInvoked++; + return (object)$rowOverrides; + }; + $rev = $this->newRevision( [], $callback ); + + $this->assertSame( RevisionRecord::DELETED_TEXT, $rev->getVisibility() ); + $this->assertSame( 12, $rev->getUser()->getId() ); + $this->assertSame( 1, $callbackInvoked ); + } + +} diff --git a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php index 68632f3e68..51c483d55d 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php +++ b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php @@ -14,6 +14,7 @@ use MediaWiki\Revision\IncompleteRevisionException; use MediaWiki\Revision\MutableRevisionRecord; use MediaWiki\Revision\RevisionArchiveRecord; use MediaWiki\Revision\RevisionRecord; +use MediaWiki\Revision\RevisionStoreRecord; use MediaWiki\Revision\RevisionSlots; use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\SlotRecord; @@ -80,7 +81,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { $this->setMwGlobals( [ 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(), 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(), - 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ] ); @@ -1706,4 +1706,195 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { $this->testNewMutableRevisionFromArray( $array ); } + /** + * Creates a new revision for testing caching behavior + * + * @param WikiPage $page the page for the new revision + * @param RevisionStore $store store object to use for creating the revision + * @return bool|RevisionStoreRecord the revision created, or false if missing + */ + private function createRevisionStoreCacheRecord( $page, $store ) { + $user = MediaWikiTestCase::getMutableTestUser()->getUser(); + $updater = $page->newPageUpdater( $user ); + $updater->setContent( SlotRecord::MAIN, new WikitextContent( __METHOD__ ) ); + $summary = CommentStoreComment::newUnsavedComment( __METHOD__ ); + $rev = $updater->saveRevision( $summary, EDIT_NEW ); + return $store->getKnownCurrentRevision( $page->getTitle(), $rev->getId() ); + } + + /** + * @covers \MediaWiki\Revision\RevisionStore::getKnownCurrentRevision + */ + public function testGetKnownCurrentRevision_userNameChange() { + global $wgActorTableSchemaMigrationStage; + + $this->overrideMwServices(); + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $this->setService( 'MainWANObjectCache', $cache ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = $this->getNonexistingTestPage(); + $rev = $this->createRevisionStoreCacheRecord( $page, $store ); + + // Grab the user name + $userNameBefore = $rev->getUser()->getName(); + + // Change the user name in the database, "behind the back" of the cache + $newUserName = "Renamed $userNameBefore"; + $this->db->update( 'revision', + [ 'rev_user_text' => $newUserName ], + [ 'rev_id' => $rev->getId() ] ); + $this->db->update( 'user', + [ 'user_name' => $newUserName ], + [ 'user_id' => $rev->getUser()->getId() ] ); + if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { + $this->db->update( 'actor', + [ 'actor_name' => $newUserName ], + [ 'actor_user' => $rev->getUser()->getId() ] ); + } + + // Reload the revision and regrab the user name. + $revAfter = $store->getKnownCurrentRevision( $page->getTitle(), $rev->getId() ); + $userNameAfter = $revAfter->getUser()->getName(); + + // The two user names should be different. + // If they are the same, we are seeing a cached value, which is bad. + $this->assertNotSame( $userNameBefore, $userNameAfter ); + + // This is implied by the above assertion, but explicitly check it, for completeness + $this->assertSame( $newUserName, $userNameAfter ); + } + + /** + * @covers \MediaWiki\Revision\RevisionStore::getKnownCurrentRevision + */ + public function testGetKnownCurrentRevision_revDelete() { + $this->overrideMwServices(); + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $this->setService( 'MainWANObjectCache', $cache ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = $this->getNonexistingTestPage(); + $rev = $this->createRevisionStoreCacheRecord( $page, $store ); + + // Grab the deleted bitmask + $deletedBefore = $rev->getVisibility(); + + // Change the deleted bitmask in the database, "behind the back" of the cache + $this->db->update( 'revision', + [ 'rev_deleted' => RevisionRecord::DELETED_TEXT ], + [ 'rev_id' => $rev->getId() ] ); + + // Reload the revision and regrab the visibility flag. + $revAfter = $store->getKnownCurrentRevision( $page->getTitle(), $rev->getId() ); + $deletedAfter = $revAfter->getVisibility(); + + // The two deleted flags should be different. + // If they are the same, we are seeing a cached value, which is bad. + $this->assertNotSame( $deletedBefore, $deletedAfter ); + + // This is implied by the above assertion, but explicitly check it, for completeness + $this->assertSame( RevisionRecord::DELETED_TEXT, $deletedAfter ); + } + + /** + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + */ + public function testNewRevisionFromRow_userNameChange() { + global $wgActorTableSchemaMigrationStage; + + $page = $this->getTestPage(); + $text = __METHOD__; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + + // Grab the user name + $userNameBefore = $record->getUser()->getName(); + + // Change the user name in the database + $newUserName = "Renamed $userNameBefore"; + $this->db->update( 'revision', + [ 'rev_user_text' => $newUserName ], + [ 'rev_id' => $record->getId() ] ); + $this->db->update( 'user', + [ 'user_name' => $newUserName ], + [ 'user_id' => $record->getUser()->getId() ] ); + if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { + $this->db->update( 'actor', + [ 'actor_name' => $newUserName ], + [ 'actor_user' => $record->getUser()->getId() ] ); + } + + // Reload the record, passing $fromCache as true to force fresh info from the db, + // and regrab the user name + $recordAfter = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle(), + true + ); + $userNameAfter = $recordAfter->getUser()->getName(); + + // The two user names should be different. + // If they are the same, we are seeing a cached value, which is bad. + $this->assertNotSame( $userNameBefore, $userNameAfter ); + + // This is implied by the above assertion, but explicitly check it, for completeness + $this->assertSame( $newUserName, $userNameAfter ); + } + + /** + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + */ + public function testNewRevisionFromRow_revDelete() { + $page = $this->getTestPage(); + $text = __METHOD__; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + + // Grab the deleted bitmask + $deletedBefore = $record->getVisibility(); + + // Change the deleted bitmask in the database + $this->db->update( 'revision', + [ 'rev_deleted' => RevisionRecord::DELETED_TEXT ], + [ 'rev_id' => $record->getId() ] ); + + // Reload the record, passing $fromCache as true to force fresh info from the db, + // and regrab the deleted bitmask + $recordAfter = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle(), + true + ); + $deletedAfter = $recordAfter->getVisibility(); + + // The two deleted flags should be different, because we modified the database. + $this->assertNotSame( $deletedBefore, $deletedAfter ); + + // This is implied by the above assertion, but explicitly check it, for completeness + $this->assertSame( RevisionRecord::DELETED_TEXT, $deletedAfter ); + } + } diff --git a/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php b/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php index 2e61745d22..138d6bcba1 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php +++ b/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php @@ -23,6 +23,9 @@ use Wikimedia\TestingAccessWrapper; class RevisionStoreFactoryTest extends MediaWikiTestCase { + /** + * @covers \MediaWiki\Revision\RevisionStoreFactory::__construct + */ public function testValidConstruction_doesntCauseErrors() { new RevisionStoreFactory( $this->getMockLoadBalancerFactory(), @@ -49,6 +52,7 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase { /** * @dataProvider provideWikiIds + * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore */ public function testGetRevisionStore( $wikiId, diff --git a/tests/phpunit/includes/Revision/RevisionStoreTest.php b/tests/phpunit/includes/Revision/RevisionStoreTest.php index efc2952856..5246e36832 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreTest.php +++ b/tests/phpunit/includes/Revision/RevisionStoreTest.php @@ -21,6 +21,9 @@ use Wikimedia\Rdbms\LoadBalancer; use Wikimedia\TestingAccessWrapper; use WikitextContent; +/** + * Tests RevisionStore + */ class RevisionStoreTest extends MediaWikiTestCase { private function useTextId() { @@ -146,6 +149,9 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() ); } + /** + * @covers \MediaWiki\Revision\RevisionStore::getTitle + */ public function testGetTitle_successFromPageId() { $mockLoadBalancer = $this->getMockLoadBalancer(); // Title calls wfGetDB() so we have to set the main service @@ -177,6 +183,9 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->assertSame( 'Food', $title->getDBkey() ); } + /** + * @covers \MediaWiki\Revision\RevisionStore::getTitle + */ public function testGetTitle_successFromPageIdOnFallback() { $mockLoadBalancer = $this->getMockLoadBalancer(); // Title calls wfGetDB() so we have to set the main service @@ -233,6 +242,9 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->assertSame( 'Foodey', $title->getDBkey() ); } + /** + * @covers \MediaWiki\Revision\RevisionStore::getTitle + */ public function testGetTitle_successFromRevId() { $mockLoadBalancer = $this->getMockLoadBalancer(); // Title calls wfGetDB() so we have to set the main service @@ -278,6 +290,9 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->assertSame( 'Food2', $title->getDBkey() ); } + /** + * @covers \MediaWiki\Revision\RevisionStore::getTitle + */ public function testGetTitle_successFromRevIdOnFallback() { $mockLoadBalancer = $this->getMockLoadBalancer(); // Title calls wfGetDB() so we have to set the main service @@ -513,12 +528,10 @@ class RevisionStoreTest extends MediaWikiTestCase { 'old_text' => 'Hello World', 'old_flags' => 'utf-8', ]; - } else { - if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) { - $row['content'] = [ - 'main' => new WikitextContent( $array['old_text'] ), - ]; - } + } elseif ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) { + $row['content'] = [ + 'main' => new WikitextContent( $array['old_text'] ), + ]; } return (object)$row; diff --git a/tests/phpunit/includes/Revision/SlotRecordTest.php b/tests/phpunit/includes/Revision/SlotRecordTest.php index ea26808355..1b6ff2aace 100644 --- a/tests/phpunit/includes/Revision/SlotRecordTest.php +++ b/tests/phpunit/includes/Revision/SlotRecordTest.php @@ -39,7 +39,7 @@ class SlotRecordTest extends MediaWikiTestCase { $this->assertTrue( $record->hasContentId() ); $this->assertTrue( $record->hasRevision() ); $this->assertTrue( $record->isInherited() ); - $this->assertSame( 'A', $record->getContent()->getNativeData() ); + $this->assertSame( 'A', $record->getContent()->getText() ); $this->assertSame( 5, $record->getSize() ); $this->assertSame( 'someHash', $record->getSha1() ); $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() ); @@ -75,7 +75,7 @@ class SlotRecordTest extends MediaWikiTestCase { $this->assertTrue( $record->hasRevision() ); $this->assertFalse( $record->hasContentId() ); $this->assertFalse( $record->isInherited() ); - $this->assertSame( 'A', $record->getContent()->getNativeData() ); + $this->assertSame( 'A', $record->getContent()->getText() ); $this->assertSame( 1, $record->getSize() ); $this->assertNotNull( $record->getSha1() ); $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() ); @@ -94,7 +94,7 @@ class SlotRecordTest extends MediaWikiTestCase { $this->assertFalse( $record->hasRevision() ); $this->assertFalse( $record->isInherited() ); $this->assertFalse( $record->hasOrigin() ); - $this->assertSame( 'A', $record->getContent()->getNativeData() ); + $this->assertSame( 'A', $record->getContent()->getText() ); $this->assertSame( 1, $record->getSize() ); $this->assertNotNull( $record->getSha1() ); $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() ); @@ -237,7 +237,7 @@ class SlotRecordTest extends MediaWikiTestCase { $this->assertTrue( $saved->hasContentId() ); $this->assertSame( 'theNewAddress', $saved->getAddress() ); $this->assertSame( 20, $saved->getContentId() ); - $this->assertSame( 'A', $saved->getContent()->getNativeData() ); + $this->assertSame( 'A', $saved->getContent()->getText() ); $this->assertSame( 10, $saved->getRevision() ); $this->assertSame( 10, $saved->getOrigin() ); diff --git a/tests/phpunit/includes/RevisionDbTestBase.php b/tests/phpunit/includes/RevisionDbTestBase.php index a2f27965c1..d7f4fd62ea 100644 --- a/tests/phpunit/includes/RevisionDbTestBase.php +++ b/tests/phpunit/includes/RevisionDbTestBase.php @@ -26,6 +26,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { [ 'page', 'revision', + 'comment', 'ip_changes', 'text', 'archive', @@ -90,7 +91,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { $this->setMwGlobals( [ 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(), 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(), - 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW, 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD, ] ); @@ -572,28 +572,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { ); } - /** - * @covers Revision::fetchRevision - */ - public function testFetchRevision() { - // Hidden process cache assertion below - $this->testPage->getRevision()->getId(); - - $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); - $id = $this->testPage->getRevision()->getId(); - - $this->hideDeprecated( 'Revision::fetchRevision' ); - $res = Revision::fetchRevision( $this->testPage->getTitle() ); - - # note: order is unspecified - $rows = []; - while ( ( $row = $res->fetchObject() ) ) { - $rows[$row->rev_id] = $row; - } - - $this->assertEmpty( $rows, 'expected empty set' ); - } - /** * @covers Revision::getPage */ @@ -680,7 +658,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { 'new null revision should have the same SHA1 as the original revision' ); $this->assertTrue( $orig->getRevisionRecord()->hasSameContent( $rev->getRevisionRecord() ), 'new null revision should have the same content as the original revision' ); - $this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() ); + $this->assertEquals( __METHOD__, $rev->getContent()->getText() ); } /** @@ -1402,7 +1380,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { ); $rev = $this->testPage->getRevision(); - $this->assertSame( $expectedText, $rev->getContent()->getNativeData() ); + $this->assertSame( $expectedText, $rev->getContent()->getText() ); $this->assertSame( $expectedText, $rev->getSerializedData() ); $this->assertSame( $this->testPage->getContentModel(), $rev->getContentModel() ); $this->assertSame( $this->testPage->getContent()->getDefaultFormat(), $rev->getContentFormat() ); @@ -1419,6 +1397,9 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { $this->setService( 'MainWANObjectCache', $cache ); $db = wfGetDB( DB_MASTER ); + $now = 1553893742; + $cache->setMockTime( $now ); + // Get a fresh revision to use during testing $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); $rev = $this->testPage->getRevision(); @@ -1433,6 +1414,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { $cache->delete( $key, WANObjectCache::HOLDOFF_NONE ); $this->assertFalse( $cache->get( $key ) ); + ++$now; + // Get the new revision and make sure it is in the cache and correct $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() ); $this->assertRevEquals( $rev, $newRev ); diff --git a/tests/phpunit/includes/RevisionMcrReadNewDbTest.php b/tests/phpunit/includes/RevisionMcrReadNewDbTest.php index 64de85451f..228bcaa07d 100644 --- a/tests/phpunit/includes/RevisionMcrReadNewDbTest.php +++ b/tests/phpunit/includes/RevisionMcrReadNewDbTest.php @@ -51,7 +51,7 @@ class RevisionMcrReadNewDbTest extends RevisionDbTestBase { [ 'tables' => [ 'text' ], 'fields' => [ 'old_id', 'old_text', 'old_flags', 'rev_text_id' ], - 'joins' => [ 'text' => [ 'INNER JOIN', 'old_id=rev_text_id' ] ] + 'joins' => [ 'text' => [ 'JOIN', 'old_id=rev_text_id' ] ] ] ]; } diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php index 20689d608b..02a6c19d1f 100644 --- a/tests/phpunit/includes/RevisionTest.php +++ b/tests/phpunit/includes/RevisionTest.php @@ -281,7 +281,6 @@ class RevisionTest extends MediaWikiTestCase { * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray */ public function testConstructFromRowWithBadPageId() { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_NEW ); $this->overrideMwServices(); Wikimedia\suppressWarnings(); $rev = new Revision( (object)[ @@ -602,7 +601,6 @@ class RevisionTest extends MediaWikiTestCase { * @covers Revision::loadFromTitle */ public function testLoadFromTitle() { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_NEW ); $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD ); $this->overrideMwServices(); $title = $this->getMockTitle(); diff --git a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php index 3339749377..92c6f62c62 100644 --- a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php +++ b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php @@ -76,7 +76,7 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase { * Creates a revision in the database. * * @param WikiPage $page - * @param $summary + * @param string|Message|CommentStoreComment $summary * @param null|string|Content $content * * @return RevisionRecord|null diff --git a/tests/phpunit/includes/Storage/NameTableStoreTest.php b/tests/phpunit/includes/Storage/NameTableStoreTest.php index 1517964471..4c2494a963 100644 --- a/tests/phpunit/includes/Storage/NameTableStoreTest.php +++ b/tests/phpunit/includes/Storage/NameTableStoreTest.php @@ -208,7 +208,7 @@ class NameTableStoreTest extends MediaWikiTestCase { public function provideGetName() { return [ - [ new HashBagOStuff(), 3, 3 ], + [ new HashBagOStuff(), 3, 2 ], [ new EmptyBagOStuff(), 3, 3 ], ]; } @@ -217,26 +217,27 @@ class NameTableStoreTest extends MediaWikiTestCase { * @dataProvider provideGetName */ public function testGetName( $cacheBag, $insertCalls, $selectCalls ) { + // Check for operations to in-memory cache (IMC) and persistent cache (PC) $store = $this->getNameTableSqlStore( $cacheBag, $insertCalls, $selectCalls ); // Get 1 ID and make sure getName returns correctly - $fooId = $store->acquireId( 'foo' ); - $this->assertSame( 'foo', $store->getName( $fooId ) ); + $fooId = $store->acquireId( 'foo' ); // regen PC, set IMC, update IMC, tombstone PC + $this->assertSame( 'foo', $store->getName( $fooId ) ); // use IMC // Get another ID and make sure getName returns correctly - $barId = $store->acquireId( 'bar' ); - $this->assertSame( 'bar', $store->getName( $barId ) ); + $barId = $store->acquireId( 'bar' ); // update IMC, tombstone PC + $this->assertSame( 'bar', $store->getName( $barId ) ); // use IMC // Blitz the cache and make sure it still returns - TestingAccessWrapper::newFromObject( $store )->tableCache = null; - $this->assertSame( 'foo', $store->getName( $fooId ) ); - $this->assertSame( 'bar', $store->getName( $barId ) ); + TestingAccessWrapper::newFromObject( $store )->tableCache = null; // clear IMC + $this->assertSame( 'foo', $store->getName( $fooId ) ); // regen interim PC, set IMC + $this->assertSame( 'bar', $store->getName( $barId ) ); // use IMC // Blitz the cache again and get another ID and make sure getName returns correctly - TestingAccessWrapper::newFromObject( $store )->tableCache = null; - $bazId = $store->acquireId( 'baz' ); - $this->assertSame( 'baz', $store->getName( $bazId ) ); - $this->assertSame( 'baz', $store->getName( $bazId ) ); + TestingAccessWrapper::newFromObject( $store )->tableCache = null; // clear IMC + $bazId = $store->acquireId( 'baz' ); // set IMC using interim PC, update IMC, tombstone PC + $this->assertSame( 'baz', $store->getName( $bazId ) ); // uses IMC + $this->assertSame( 'baz', $store->getName( $bazId ) ); // uses IMC } public function testGetName_masterFallback() { diff --git a/tests/phpunit/includes/Storage/PageUpdaterTest.php b/tests/phpunit/includes/Storage/PageUpdaterTest.php index 89e1d4ee1f..9d60605917 100644 --- a/tests/phpunit/includes/Storage/PageUpdaterTest.php +++ b/tests/phpunit/includes/Storage/PageUpdaterTest.php @@ -241,7 +241,7 @@ class PageUpdaterTest extends MediaWikiTestCase { * Creates a revision in the database. * * @param WikiPage $page - * @param $summary + * @param string|Message|CommentStoreComment $summary * @param null|string|Content $content * * @return RevisionRecord|null diff --git a/tests/phpunit/includes/TestUser.php b/tests/phpunit/includes/TestUser.php index 952a662fcd..773bd519ab 100644 --- a/tests/phpunit/includes/TestUser.php +++ b/tests/phpunit/includes/TestUser.php @@ -143,7 +143,7 @@ class TestUser { } $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory(); - if ( !$passwordFactory->newFromCiphertext( $row->user_password )->equals( $password ) ) { + if ( !$passwordFactory->newFromCiphertext( $row->user_password )->verify( $password ) ) { $passwordHash = $passwordFactory->newFromPlaintext( $password ); $dbw->update( 'user', diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php index 25dc9b3e30..abd70b2524 100644 --- a/tests/phpunit/includes/TitleMethodsTest.php +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -31,30 +31,6 @@ class TitleMethodsTest extends MediaWikiLangTestCase { ); } - public static function provideEquals() { - return [ - [ 'Main Page', 'Main Page', true ], - [ 'Main Page', 'Not The Main Page', false ], - [ 'Main Page', 'Project:Main Page', false ], - [ 'File:Example.png', 'Image:Example.png', true ], - [ 'Special:Version', 'Special:Version', true ], - [ 'Special:Version', 'Special:Recentchanges', false ], - [ 'Special:Version', 'Main Page', false ], - ]; - } - - /** - * @dataProvider provideEquals - * @covers Title::equals - */ - public function testEquals( $titleA, $titleB, $expectedBool ) { - $titleA = Title::newFromText( $titleA ); - $titleB = Title::newFromText( $titleB ); - - $this->assertEquals( $expectedBool, $titleA->equals( $titleB ) ); - $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) ); - } - public static function provideInNamespace() { return [ [ 'Main Page', NS_MAIN, true ], diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php index cb5e1f8a79..f7ffe8d434 100644 --- a/tests/phpunit/includes/TitlePermissionTest.php +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -6,8 +6,8 @@ use MediaWiki\MediaWikiServices; /** * @group Database * - * @covers Title::getUserPermissionsErrors - * @covers Title::getUserPermissionsErrorsInternal + * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrors + * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrorsInternal */ class TitlePermissionTest extends MediaWikiLangTestCase { @@ -104,7 +104,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { * This test is failing per T201776. * * @group Broken - * @covers Title::checkQuickPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions */ public function testQuickPermissions() { $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> @@ -395,7 +395,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkSpecialsAndNSPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions */ public function testSpecialsAndNSPermissions() { global $wgNamespaceProtection; @@ -452,7 +452,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testJsConfigEditPermissions() { $this->setUser( $this->userName ); @@ -475,7 +475,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testJsonConfigEditPermissions() { $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> @@ -500,7 +500,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testCssConfigEditPermissions() { $this->setUser( $this->userName ); @@ -523,7 +523,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherJsConfigEditPermissions() { $this->setUser( $this->userName ); @@ -546,7 +546,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherJsonConfigEditPermissions() { $this->setUser( $this->userName ); @@ -569,7 +569,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherCssConfigEditPermissions() { $this->setUser( $this->userName ); @@ -592,7 +592,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testOtherNonConfigEditPermissions() { $this->setUser( $this->userName ); @@ -614,7 +614,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This should use data providers like the other methods here. - * @covers Title::checkUserConfigPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions */ public function testPatrolActionConfigEditPermissions() { $this->setUser( 'anon' ); @@ -687,7 +687,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { * This test is failing per T201776. * * @group Broken - * @covers Title::checkPageRestrictions + * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions */ public function testPageRestrictions() { $prefix = MediaWikiServices::getInstance()->getContentLanguage()-> @@ -780,7 +780,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { } /** - * @covers Title::checkCascadingSourcesRestrictions + * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions */ public function testCascadingSourcesRestrictions() { $this->setTitle( NS_MAIN, "test page" ); @@ -811,7 +811,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers - * @covers Title::checkActionPermissions + * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions */ public function testActionPermissions() { $this->setUserPerm( [ "createpage" ] ); @@ -885,13 +885,14 @@ class TitlePermissionTest extends MediaWikiLangTestCase { } /** - * @covers Title::checkUserBlock + * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock */ public function testUserBlock() { $this->setMwGlobals( [ 'wgEmailConfirmToEdit' => true, 'wgEmailAuthentication' => true, ] ); + $this->overrideMwServices(); $this->setUserPerm( [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ] ); $this->setTitle( NS_HELP, "test page" ); @@ -903,6 +904,8 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); $this->setMwGlobals( 'wgEmailConfirmToEdit', false ); + $this->overrideMwServices(); + $this->assertNotContains( [ 'confirmedittext' ], $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); @@ -923,7 +926,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { 'auto' => true, 'expiry' => 0 ] ); - $this->user->mBlock->mTimestamp = 0; + $this->user->mBlock->setTimestamp( 0 ); $this->assertEquals( [ [ 'autoblockedtext', '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', 'Useruser', null, 'infinite', '127.0.8.1', @@ -1029,5 +1032,62 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->title->getUserPermissionsErrors( 'upload', $this->user ) ); $this->assertEquals( [], $this->title->getUserPermissionsErrors( 'purge', $this->user ) ); + + // Test no block. + $this->user->mBlockedby = null; + $this->user->mBlock = null; + + $this->assertEquals( [], + $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock + * + * Tests to determine that the passed in permission does not get mixed up with + * an action of the same name. + */ + public function testUserBlockAction() { + global $wgLang; + + $tester = $this->getMockBuilder( Action::class ) + ->disableOriginalConstructor() + ->getMock(); + $tester->method( 'getName' ) + ->willReturn( 'tester' ); + $tester->method( 'getRestriction' ) + ->willReturn( 'test' ); + $tester->method( 'requiresUnblock' ) + ->willReturn( false ); + + $this->setMwGlobals( [ + 'wgActions' => [ + 'tester' => $tester, + ], + 'wgGroupPermissions' => [ + '*' => [ + 'tester' => true, + ], + ], + ] ); + + $now = time(); + $this->user->mBlockedby = $this->user->getName(); + $this->user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $this->user->getId(), + 'reason' => 'no reason given', + 'timestamp' => $now, + 'auto' => false, + 'expiry' => 'infinity', + ] ); + + $errors = [ [ 'blockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, 'infinite', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ]; + + $this->assertEquals( $errors, + $this->title->getUserPermissionsErrors( 'tester', $this->user ) ); } } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index f36fbfd1d5..149c25bc5f 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -1,5 +1,6 @@ setMwGlobals( 'wgContentHandlerUseDB', false ); @@ -315,7 +315,6 @@ class TitleTest extends MediaWikiTestCase { [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ], [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ], [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ], - // for Title::validateFileMoveOperation [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ], ]; } @@ -328,7 +327,7 @@ class TitleTest extends MediaWikiTestCase { * @param string $action * @param array|string|bool $expected Required error * - * @covers Title::checkReadPermissions + * @covers \Mediawiki\Permissions\PermissionManager::checkReadPermissions * @dataProvider dataWgWhitelistReadRegexp */ public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) { @@ -566,6 +565,32 @@ class TitleTest extends MediaWikiTestCase { $this->assertEquals( $value->getFragment(), $title->getFragment() ); } + /** + * @covers Title::newFromLinkTarget + * @dataProvider provideNewFromTitleValue + */ + public function testNewFromLinkTarget( LinkTarget $value ) { + $title = Title::newFromLinkTarget( $value ); + + $dbkey = str_replace( ' ', '_', $value->getText() ); + $this->assertEquals( $dbkey, $title->getDBkey() ); + $this->assertEquals( $value->getNamespace(), $title->getNamespace() ); + $this->assertEquals( $value->getFragment(), $title->getFragment() ); + } + + /** + * @covers Title::newFromLinkTarget + */ + public function testNewFromLinkTarget_clone() { + $title = Title::newFromText( __METHOD__ ); + $this->assertSame( $title, Title::newFromLinkTarget( $title ) ); + + // The Title::NEW_CLONE flag should ensure that a fresh instance is returned. + $clone = Title::newFromLinkTarget( $title, Title::NEW_CLONE ); + $this->assertNotSame( $title, $clone ); + $this->assertTrue( $clone->equals( $title ) ); + } + public static function provideGetTitleValue() { return [ [ 'Foo' ], @@ -730,18 +755,6 @@ class TitleTest extends MediaWikiTestCase { $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() ); } - /** - * @dataProvider provideCanHaveTalkPage - * @covers Title::canTalk - * - * @param Title $title - * @param bool $expected - */ - public function testCanTalk( Title $title, $expected ) { - $actual = $title->canTalk(); - $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() ); - } - public static function provideGetTalkPage_good() { return [ [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ], @@ -995,4 +1008,93 @@ class TitleTest extends MediaWikiTestCase { [ 'Main Page', false ], ]; } + + public function provideEquals() { + yield [ + Title::newFromText( 'Main Page' ), + Title::newFromText( 'Main Page' ), + true + ]; + yield [ + Title::newFromText( 'Main Page' ), + Title::newFromText( 'Not The Main Page' ), + false + ]; + yield [ + Title::newFromText( 'Main Page' ), + Title::newFromText( 'Project:Main Page' ), + false + ]; + yield [ + Title::newFromText( 'File:Example.png' ), + Title::newFromText( 'Image:Example.png' ), + true + ]; + yield [ + Title::newFromText( 'Special:Version' ), + Title::newFromText( 'Special:Version' ), + true + ]; + yield [ + Title::newFromText( 'Special:Version' ), + Title::newFromText( 'Special:Recentchanges' ), + false + ]; + yield [ + Title::newFromText( 'Special:Version' ), + Title::newFromText( 'Main Page' ), + false + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', '', '' ), + Title::makeTitle( NS_MAIN, 'Foo', '', '' ), + true + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', '', '' ), + Title::makeTitle( NS_MAIN, 'Bar', '', '' ), + false + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', '', '' ), + Title::makeTitle( NS_TALK, 'Foo', '', '' ), + false + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), + Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), + true + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), + Title::makeTitle( NS_MAIN, 'Foo', 'Baz', '' ), + true + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), + Title::makeTitle( NS_MAIN, 'Foo', '', '' ), + true + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ), + Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ), + true + ]; + yield [ + Title::makeTitle( NS_MAIN, 'Foo', '', '' ), + Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ), + false + ]; + } + + /** + * @covers Title::equals + * @dataProvider provideEquals + */ + public function testEquals( Title $firstValue, /* LinkTarget */ $secondValue, $expectedSame ) { + $this->assertSame( + $expectedSame, + $firstValue->equals( $secondValue ) + ); + } } diff --git a/tests/phpunit/includes/WikiMapTest.php b/tests/phpunit/includes/WikiMapTest.php index 1fb8affa39..1608b9c955 100644 --- a/tests/phpunit/includes/WikiMapTest.php +++ b/tests/phpunit/includes/WikiMapTest.php @@ -237,41 +237,44 @@ class WikiMapTest extends MediaWikiLangTestCase { public function provideGetWikiIdFromDomain() { return [ - [ 'db-prefix', 'db-prefix' ], + [ 'db-prefix_', 'db-prefix_' ], [ wfWikiID(), wfWikiID() ], - [ new DatabaseDomain( 'db-dash', null, 'prefix' ), 'db-dash-prefix' ], + [ new DatabaseDomain( 'db-dash', null, 'prefix_' ), 'db-dash-prefix_' ], [ wfWikiID(), wfWikiID() ], - [ new DatabaseDomain( 'db-dash', null, 'prefix' ), 'db-dash-prefix' ], - [ new DatabaseDomain( 'db', 'mediawiki', 'prefix' ), 'db-prefix' ], // schema ignored - [ new DatabaseDomain( 'db', 'custom', 'prefix' ), 'db-custom-prefix' ], + [ new DatabaseDomain( 'db-dash', null, 'prefix_' ), 'db-dash-prefix_' ], + [ new DatabaseDomain( 'db', 'mediawiki', 'prefix_' ), 'db-prefix_' ], // schema ignored + [ new DatabaseDomain( 'db', 'custom', 'prefix_' ), 'db-custom-prefix_' ], ]; } /** * @dataProvider provideGetWikiIdFromDomain - * @covers WikiMap::getWikiIdFromDomain() + * @covers WikiMap::getWikiIdFromDbDomain() */ public function testGetWikiIdFromDomain( $domain, $wikiId ) { - $this->assertEquals( $wikiId, WikiMap::getWikiIdFromDomain( $domain ) ); + $this->assertEquals( $wikiId, WikiMap::getWikiIdFromDbDomain( $domain ) ); } /** - * @covers WikiMap::isCurrentWikiDomain() - * @covers WikiMap::getCurrentWikiDomain() + * @covers WikiMap::isCurrentWikiDbDomain() + * @covers WikiMap::getCurrentWikiDbDomain() */ public function testIsCurrentWikiDomain() { - $this->assertTrue( WikiMap::isCurrentWikiDomain( wfWikiID() ) ); + $this->setMwGlobals( 'wgDBmwschema', 'mediawiki' ); - $localDomain = DatabaseDomain::newFromId( wfWikiID() ); + $localDomain = WikiMap::getCurrentWikiDbDomain()->getId(); + $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $localDomain ) ); + + $localDomain = DatabaseDomain::newFromId( $localDomain ); $domain1 = new DatabaseDomain( $localDomain->getDatabase(), 'someschema', $localDomain->getTablePrefix() ); $domain2 = new DatabaseDomain( $localDomain->getDatabase(), null, $localDomain->getTablePrefix() ); - $this->assertTrue( WikiMap::isCurrentWikiDomain( $domain1 ), 'Schema ignored' ); - $this->assertTrue( WikiMap::isCurrentWikiDomain( $domain2 ), 'Schema ignored' ); + $this->assertFalse( WikiMap::isCurrentWikiDbDomain( $domain1 ), 'Schema not ignored' ); + $this->assertFalse( WikiMap::isCurrentWikiDbDomain( $domain2 ), 'Null schema not ignored' ); - $this->assertTrue( WikiMap::isCurrentWikiDomain( WikiMap::getCurrentWikiDomain() ) ); + $this->assertTrue( WikiMap::isCurrentWikiDbDomain( WikiMap::getCurrentWikiDbDomain() ) ); } public function provideIsCurrentWikiId() { @@ -279,23 +282,23 @@ class WikiMapTest extends MediaWikiLangTestCase { [ 'db', 'db', null, '' ], [ 'db-schema-','db', 'schema', '' ], [ 'db','db', 'mediawiki', '' ], // common b/c case - [ 'db-prefix', 'db', null, 'prefix' ], - [ 'db-schema-prefix', 'db', 'schema', 'prefix' ], - [ 'db-prefix', 'db', 'mediawiki', 'prefix' ], // common b/c case + [ 'db-prefix_', 'db', null, 'prefix_' ], + [ 'db-schema-prefix_', 'db', 'schema', 'prefix_' ], + [ 'db-prefix_', 'db', 'mediawiki', 'prefix_' ], // common b/c case // Bad hyphen cases (best effort support) [ 'db-stuff', 'db-stuff', null, '' ], - [ 'db-stuff-prefix', 'db-stuff', null, 'prefix' ], + [ 'db-stuff-prefix_', 'db-stuff', null, 'prefix_' ], [ 'db-stuff-schema-', 'db-stuff', 'schema', '' ], - [ 'db-stuff-schema-prefix', 'db-stuff', 'schema', 'prefix' ], - [ 'db-stuff-prefix', 'db-stuff', 'mediawiki', 'prefix' ] // common b/c case + [ 'db-stuff-schema-prefix_', 'db-stuff', 'schema', 'prefix_' ], + [ 'db-stuff-prefix_', 'db-stuff', 'mediawiki', 'prefix_' ] // common b/c case ]; } /** * @dataProvider provideIsCurrentWikiId * @covers WikiMap::isCurrentWikiId() - * @covers WikiMap::getCurrentWikiDomain() - * @covers WikiMap::getWikiIdFromDomain() + * @covers WikiMap::getCurrentWikiDbDomain() + * @covers WikiMap::getWikiIdFromDbDomain() */ public function testIsCurrentWikiId( $wikiId, $db, $schema, $prefix ) { $this->setMwGlobals( diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php index 4556473af1..ab9abbb429 100644 --- a/tests/phpunit/includes/XmlTest.php +++ b/tests/phpunit/includes/XmlTest.php @@ -454,6 +454,34 @@ class XmlTest extends MediaWikiTestCase { ); } + /** + * @covers Xml::encodeJsVar + */ + public function testXmlJsCode() { + $code = 'function () { foo( 42 ); }'; + $this->assertEquals( + $code, + Xml::encodeJsVar( new XmlJsCode( $code ) ) + ); + } + + /** + * @covers Xml::encodeJsVar + * @covers XmlJsCode::encodeObject + */ + public function testEncodeObject() { + $codeA = 'function () { foo( 42 ); }'; + $codeB = 'function ( jQuery ) { bar( 142857 ); }'; + $obj = XmlJsCode::encodeObject( [ + 'a' => new XmlJsCode( $codeA ), + 'b' => new XmlJsCode( $codeB ) + ] ); + $this->assertEquals( + "{\"a\":$codeA,\"b\":$codeB}", + Xml::encodeJsVar( $obj ) + ); + } + /** * @covers Xml::listDropDown */ diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php index 121820ad48..0dc64df87f 100644 --- a/tests/phpunit/includes/api/ApiBaseTest.php +++ b/tests/phpunit/includes/api/ApiBaseTest.php @@ -529,12 +529,36 @@ class ApiBaseTest extends ApiTestCase { 'foo', [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ], ], + 'Deprecated parameter with default, unspecified' => [ + null, + [ ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => 'foo' ], + 'foo', + [], + ], + 'Deprecated parameter with default, specified' => [ + 'foo', + [ ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => 'foo' ], + 'foo', + [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ], + ], 'Deprecated parameter value' => [ 'a', [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ], 'a', [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ], ], + 'Deprecated parameter value as default, unspecified' => [ + null, + [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ], ApiBase::PARAM_DFLT => 'a' ], + 'a', + [], + ], + 'Deprecated parameter value as default, specified' => [ + 'a', + [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ], ApiBase::PARAM_DFLT => 'a' ], + 'a', + [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ], + ], 'Multiple deprecated parameter values' => [ 'a|b|c|d', [ ApiBase::PARAM_DEPRECATED_VALUES => @@ -1273,6 +1297,8 @@ class ApiBaseTest extends ApiTestCase { public function testErrorArrayToStatus() { $mock = new MockApi(); + $msg = new Message( 'mainpage' ); + // Sanity check empty array $expect = Status::newGood(); $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) ); @@ -1283,12 +1309,16 @@ class ApiBaseTest extends ApiTestCase { $expect->fatal( 'autoblockedtext' ); $expect->fatal( 'systemblockedtext' ); $expect->fatal( 'mainpage' ); + $expect->fatal( $msg ); + $expect->fatal( $msg, 'foobar' ); $expect->fatal( 'parentheses', 'foobar' ); $this->assertEquals( $expect, $mock->errorArrayToStatus( [ [ 'blockedtext' ], [ 'autoblockedtext' ], [ 'systemblockedtext' ], 'mainpage', + $msg, + [ $msg, 'foobar' ], [ 'parentheses', 'foobar' ], ] ) ); @@ -1309,16 +1339,76 @@ class ApiBaseTest extends ApiTestCase { $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) ); $expect->fatal( ApiMessage::create( 'apierror-systemblocked', 'blocked', $blockinfo ) ); $expect->fatal( 'mainpage' ); + $expect->fatal( $msg ); + $expect->fatal( $msg, 'foobar' ); $expect->fatal( 'parentheses', 'foobar' ); $this->assertEquals( $expect, $mock->errorArrayToStatus( [ [ 'blockedtext' ], [ 'autoblockedtext' ], [ 'systemblockedtext' ], 'mainpage', + $msg, + [ $msg, 'foobar' ], [ 'parentheses', 'foobar' ], ], $user ) ); } + public function testAddBlockInfoToStatus() { + $mock = new MockApi(); + + $msg = new Message( 'mainpage' ); + + // Sanity check empty array + $expect = Status::newGood(); + $test = Status::newGood(); + $mock->addBlockInfoToStatus( $test ); + $this->assertEquals( $expect, $test ); + + // No blocked $user, so no special block handling + $expect = Status::newGood(); + $expect->fatal( 'blockedtext' ); + $expect->fatal( 'autoblockedtext' ); + $expect->fatal( 'systemblockedtext' ); + $expect->fatal( 'mainpage' ); + $expect->fatal( $msg ); + $expect->fatal( $msg, 'foobar' ); + $expect->fatal( 'parentheses', 'foobar' ); + $test = clone $expect; + $mock->addBlockInfoToStatus( $test ); + $this->assertEquals( $expect, $test ); + + // Has a blocked $user, so special block handling + $user = $this->getMutableTestUser()->getUser(); + $block = new \Block( [ + 'address' => $user->getName(), + 'user' => $user->getID(), + 'by' => $this->getTestSysop()->getUser()->getId(), + 'reason' => __METHOD__, + 'expiry' => time() + 100500, + ] ); + $block->insert(); + $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]; + + $expect = Status::newGood(); + $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) ); + $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) ); + $expect->fatal( ApiMessage::create( 'apierror-systemblocked', 'blocked', $blockinfo ) ); + $expect->fatal( 'mainpage' ); + $expect->fatal( $msg ); + $expect->fatal( $msg, 'foobar' ); + $expect->fatal( 'parentheses', 'foobar' ); + $test = Status::newGood(); + $test->fatal( 'blockedtext' ); + $test->fatal( 'autoblockedtext' ); + $test->fatal( 'systemblockedtext' ); + $test->fatal( 'mainpage' ); + $test->fatal( $msg ); + $test->fatal( $msg, 'foobar' ); + $test->fatal( 'parentheses', 'foobar' ); + $mock->addBlockInfoToStatus( $test, $user ); + $this->assertEquals( $expect, $test ); + } + public function testDieStatus() { $mock = new MockApi(); diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php index a26f8a8188..7274a546ee 100644 --- a/tests/phpunit/includes/api/ApiBlockTest.php +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -62,7 +62,7 @@ class ApiBlockTest extends ApiTestCase { $this->assertTrue( !is_null( $block ), 'Block is valid' ); $this->assertSame( $this->mUser->getName(), (string)$block->getTarget() ); - $this->assertSame( 'Some reason', $block->mReason ); + $this->assertSame( 'Some reason', $block->getReason() ); return $ret; } @@ -131,8 +131,8 @@ class ApiBlockTest extends ApiTestCase { __METHOD__, [], [ - 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ], - 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ], + 'change_tag' => [ 'JOIN', 'ct_log_id = log_id' ], + 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ], ] ) ); } diff --git a/tests/phpunit/includes/api/ApiComparePagesTest.php b/tests/phpunit/includes/api/ApiComparePagesTest.php index 7bab542005..9e18eb0985 100644 --- a/tests/phpunit/includes/api/ApiComparePagesTest.php +++ b/tests/phpunit/includes/api/ApiComparePagesTest.php @@ -44,37 +44,37 @@ class ApiComparePagesTest extends ApiTestCase { self::$repl['revA2'] = $this->addPage( 'A', 'A 2' ); self::$repl['revA3'] = $this->addPage( 'A', 'A 3' ); self::$repl['revA4'] = $this->addPage( 'A', 'A 4' ); - self::$repl['pageA'] = Title::newFromText( 'ApiComparePagesTest A' )->getArticleId(); + self::$repl['pageA'] = Title::newFromText( 'ApiComparePagesTest A' )->getArticleID(); self::$repl['revB1'] = $this->addPage( 'B', 'B 1' ); self::$repl['revB2'] = $this->addPage( 'B', 'B 2' ); self::$repl['revB3'] = $this->addPage( 'B', 'B 3' ); self::$repl['revB4'] = $this->addPage( 'B', 'B 4' ); - self::$repl['pageB'] = Title::newFromText( 'ApiComparePagesTest B' )->getArticleId(); + self::$repl['pageB'] = Title::newFromText( 'ApiComparePagesTest B' )->getArticleID(); self::$repl['revC1'] = $this->addPage( 'C', 'C 1' ); self::$repl['revC2'] = $this->addPage( 'C', 'C 2' ); self::$repl['revC3'] = $this->addPage( 'C', 'C 3' ); - self::$repl['pageC'] = Title::newFromText( 'ApiComparePagesTest C' )->getArticleId(); + self::$repl['pageC'] = Title::newFromText( 'ApiComparePagesTest C' )->getArticleID(); $id = $this->addPage( 'D', 'D 1' ); - self::$repl['pageD'] = Title::newFromText( 'ApiComparePagesTest D' )->getArticleId(); + self::$repl['pageD'] = Title::newFromText( 'ApiComparePagesTest D' )->getArticleID(); wfGetDB( DB_MASTER )->delete( 'revision', [ 'rev_id' => $id ] ); self::$repl['revE1'] = $this->addPage( 'E', 'E 1' ); self::$repl['revE2'] = $this->addPage( 'E', 'E 2' ); self::$repl['revE3'] = $this->addPage( 'E', 'E 3' ); self::$repl['revE4'] = $this->addPage( 'E', 'E 4' ); - self::$repl['pageE'] = Title::newFromText( 'ApiComparePagesTest E' )->getArticleId(); + self::$repl['pageE'] = Title::newFromText( 'ApiComparePagesTest E' )->getArticleID(); wfGetDB( DB_MASTER )->update( 'page', [ 'page_latest' => 0 ], [ 'page_id' => self::$repl['pageE'] ] ); self::$repl['revF1'] = $this->addPage( 'F', "== Section 1 ==\nF 1.1\n\n== Section 2 ==\nF 1.2" ); - self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleId(); + self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleID(); self::$repl['revG1'] = $this->addPage( 'G', "== Section 1 ==\nG 1.1", CONTENT_MODEL_TEXT ); - self::$repl['pageG'] = Title::newFromText( 'ApiComparePagesTest G' )->getArticleId(); + self::$repl['pageG'] = Title::newFromText( 'ApiComparePagesTest G' )->getArticleID(); WikiPage::factory( Title::newFromText( 'ApiComparePagesTest C' ) ) ->doDeleteArticleReal( 'Test for ApiComparePagesTest' ); diff --git a/tests/phpunit/includes/api/ApiDeleteTest.php b/tests/phpunit/includes/api/ApiDeleteTest.php index 803eefb498..c68954c077 100644 --- a/tests/phpunit/includes/api/ApiDeleteTest.php +++ b/tests/phpunit/includes/api/ApiDeleteTest.php @@ -128,8 +128,8 @@ class ApiDeleteTest extends ApiTestCase { __METHOD__, [], [ - 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ], - 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] + 'change_tag' => [ 'JOIN', 'ct_log_id = log_id' ], + 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ] ) ); } diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php index 2161093311..aeb829dd62 100644 --- a/tests/phpunit/includes/api/ApiEditPageTest.php +++ b/tests/phpunit/includes/api/ApiEditPageTest.php @@ -152,7 +152,7 @@ class ApiEditPageTest extends ApiTestCase { $content = $page->getContent(); $this->assertNotNull( $content, 'Page should have been created' ); - $text = $content->getNativeData(); + $text = $content->getText(); $this->assertSame( $expected, $text ); } @@ -176,7 +176,7 @@ class ApiEditPageTest extends ApiTestCase { $this->assertSame( 'Success', $re['edit']['result'] ); $newtext = WikiPage::factory( Title::newFromText( $name ) ) ->getContent( Revision::RAW ) - ->getNativeData(); + ->getText(); $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext ); // Test that we raise a 'nosuchsection' error @@ -216,7 +216,7 @@ class ApiEditPageTest extends ApiTestCase { // Check the page text is correct $text = WikiPage::factory( Title::newFromText( $name ) ) ->getContent( Revision::RAW ) - ->getNativeData(); + ->getText(); $this->assertSame( "== header ==\n\ntest", $text ); // Now on one that does @@ -232,7 +232,7 @@ class ApiEditPageTest extends ApiTestCase { $this->assertSame( 'Success', $re2['edit']['result'] ); $text = WikiPage::factory( Title::newFromText( $name ) ) ->getContent( Revision::RAW ) - ->getNativeData(); + ->getText(); $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text ); } @@ -733,7 +733,7 @@ class ApiEditPageTest extends ApiTestCase { 'undoafter' => $revId1, ] ); - $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData(); + $text = ( new WikiPage( $titleObj ) )->getContent()->getText(); // This is wrong! It should be 1. But let's test for our incorrect // behavior for now, so if someone fixes it they'll fix the test as @@ -761,7 +761,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent() - ->getNativeData(); + ->getText(); $this->assertSame( '3', $text ); } @@ -784,7 +784,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent() - ->getNativeData(); + ->getText(); $this->assertSame( '1', $text ); } @@ -855,7 +855,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( 'Alert: Some text', $text ); } @@ -872,7 +872,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( 'Some text is nice', $text ); } @@ -890,7 +890,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( 'Alert: Some text is nice', $text ); } @@ -957,7 +957,7 @@ class ApiEditPageTest extends ApiTestCase { } finally { // Validate that content was not changed $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( 'Some text', $text ); } @@ -1059,7 +1059,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( "Initial content\n\n== New section ==", $text ); } @@ -1097,7 +1097,7 @@ class ApiEditPageTest extends ApiTestCase { $page = new WikiPage( Title::newFromText( $name ) ); $this->assertSame( "Initial content\n\n== My section ==\n\nMore content", - $page->getContent()->getNativeData() ); + $page->getContent()->getText() ); $this->assertSame( '/* My section */ new section', $page->getRevision()->getComment() ); } @@ -1118,7 +1118,7 @@ class ApiEditPageTest extends ApiTestCase { $page = new WikiPage( Title::newFromText( $name ) ); $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content", - $page->getContent()->getNativeData() ); + $page->getContent()->getText() ); // EditPage actually assumes the summary is the section name here $this->assertSame( '/* Add new section */ new section', $page->getRevision()->getComment() ); @@ -1141,7 +1141,7 @@ class ApiEditPageTest extends ApiTestCase { $page = new WikiPage( Title::newFromText( $name ) ); $this->assertSame( "Initial content\n\n== My section ==\n\nMore content", - $page->getContent()->getNativeData() ); + $page->getContent()->getText() ); $this->assertSame( 'Add new section', $page->getRevision()->getComment() ); } @@ -1160,7 +1160,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" . "== Section 2 ==\n\nFascinating!", $text ); @@ -1179,7 +1179,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" . "Fascinating!", $text ); @@ -1201,7 +1201,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); } finally { $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( 'Content', $text ); } @@ -1223,7 +1223,7 @@ class ApiEditPageTest extends ApiTestCase { ] ); } finally { $text = ( new WikiPage( Title::newFromText( $name ) ) ) - ->getContent()->getNativeData(); + ->getContent()->getText(); $this->assertSame( 'Content', $text ); } @@ -1349,7 +1349,7 @@ class ApiEditPageTest extends ApiTestCase { 'ctd_name', [ 'ct_rev_id' => $revId ], __METHOD__, - [ 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] ] + [ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ] ) ); } @@ -1474,8 +1474,7 @@ class ApiEditPageTest extends ApiTestCase { public function testEditWhileBlocked() { $name = 'Help:' . ucfirst( __FUNCTION__ ); - $this->setExpectedException( ApiUsageException::class, - 'You have been blocked from editing.' ); + $this->assertNull( Block::newFromTarget( '127.0.0.1' ), 'Sanity check' ); $block = new Block( [ 'address' => self::$users['sysop']->getUser()->getName(), @@ -1483,6 +1482,7 @@ class ApiEditPageTest extends ApiTestCase { 'reason' => 'Capriciousness', 'timestamp' => '19370101000000', 'expiry' => 'infinity', + 'enableAutoblock' => true, ] ); $block->insert(); @@ -1492,6 +1492,10 @@ class ApiEditPageTest extends ApiTestCase { 'title' => $name, 'text' => 'Some text', ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( ApiUsageException $ex ) { + $this->assertSame( 'You have been blocked from editing.', $ex->getMessage() ); + $this->assertNotNull( Block::newFromTarget( '127.0.0.1' ), 'Autoblock spread' ); } finally { $block->delete(); self::$users['sysop']->getUser()->clearInstanceCache(); diff --git a/tests/phpunit/includes/api/ApiErrorFormatterTest.php b/tests/phpunit/includes/api/ApiErrorFormatterTest.php index d7628e0ccf..2eec176bfc 100644 --- a/tests/phpunit/includes/api/ApiErrorFormatterTest.php +++ b/tests/phpunit/includes/api/ApiErrorFormatterTest.php @@ -634,6 +634,11 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { ]; } + /** + * @covers ApiErrorFormatter::addMessagesFromStatus + * @covers ApiErrorFormatter::addWarningOrError + * @covers ApiErrorFormatter::formatMessageInternal + */ public function testAddMessagesFromStatus_filter() { $result = new ApiResult( 8388608 ); $formatter = new ApiErrorFormatter( $result, Language::factory( 'qqx' ), 'plaintext', false ); diff --git a/tests/phpunit/includes/api/ApiMoveTest.php b/tests/phpunit/includes/api/ApiMoveTest.php index 3fa85394c1..d437a525a0 100644 --- a/tests/phpunit/includes/api/ApiMoveTest.php +++ b/tests/phpunit/includes/api/ApiMoveTest.php @@ -17,6 +17,7 @@ class ApiMoveTest extends ApiTestCase { protected function assertMoved( $from, $to, $id, $opts = null ) { $opts = (array)$opts; + Title::clearCaches(); $fromTitle = Title::newFromText( $from ); $toTitle = Title::newFromText( $to ); @@ -36,7 +37,7 @@ class ApiMoveTest extends ApiTestCase { $this->assertSame( $toTitle->getPrefixedText(), $target->getPrefixedText() ); } - $this->assertSame( $id, $toTitle->getArticleId() ); + $this->assertSame( $id, $toTitle->getArticleID() ); } /** @@ -126,7 +127,40 @@ class ApiMoveTest extends ApiTestCase { 'to' => '[', ] ); } finally { - $this->assertSame( $id, Title::newFromText( $name )->getArticleId() ); + $this->assertSame( $id, Title::newFromText( $name )->getArticleID() ); + } + } + + public function testMoveWhileBlocked() { + $this->assertNull( Block::newFromTarget( '127.0.0.1' ), 'Sanity check' ); + + $block = new Block( [ + 'address' => self::$users['sysop']->getUser()->getName(), + 'by' => self::$users['sysop']->getUser()->getId(), + 'reason' => 'Capriciousness', + 'timestamp' => '19370101000000', + 'expiry' => 'infinity', + 'enableAutoblock' => true, + ] ); + $block->insert(); + + $name = ucfirst( __FUNCTION__ ); + $id = $this->createPage( $name ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( ApiUsageException $ex ) { + $this->assertSame( 'You have been blocked from editing.', $ex->getMessage() ); + $this->assertNotNull( Block::newFromTarget( '127.0.0.1' ), 'Autoblock spread' ); + } finally { + $block->delete(); + self::$users['sysop']->getUser()->clearInstanceCache(); + $this->assertSame( $id, Title::newFromText( $name )->getArticleID() ); } } @@ -161,7 +195,7 @@ class ApiMoveTest extends ApiTestCase { 'to' => "$name 3", ] ); } finally { - $this->assertSame( $id, Title::newFromText( "$name 2" )->getArticleId() ); + $this->assertSame( $id, Title::newFromText( "$name 2" )->getArticleID() ); $this->assertFalse( Title::newFromText( "$name 3" )->exists(), "\"$name 3\" should not exist" ); } @@ -187,7 +221,7 @@ class ApiMoveTest extends ApiTestCase { 'tags' => 'custom tag', ] ); } finally { - $this->assertSame( $id, Title::newFromText( $name )->getArticleId() ); + $this->assertSame( $id, Title::newFromText( $name )->getArticleID() ); $this->assertFalse( Title::newFromText( "$name 2" )->exists(), "\"$name 2\" should not exist" ); } @@ -241,9 +275,9 @@ class ApiMoveTest extends ApiTestCase { ] ); $this->assertMoved( $name, "$name 2", $id ); - $this->assertSame( $talkId, Title::newFromText( "Talk:$name" )->getArticleId() ); + $this->assertSame( $talkId, Title::newFromText( "Talk:$name" )->getArticleID() ); $this->assertSame( $talkDestinationId, - Title::newFromText( "Talk:$name 2" )->getArticleId() ); + Title::newFromText( "Talk:$name 2" )->getArticleID() ); $this->assertSame( [ [ 'message' => 'articleexists', 'params' => [], @@ -278,9 +312,9 @@ class ApiMoveTest extends ApiTestCase { } $this->assertSame( $ids["$name/error"], - Title::newFromText( "$name/error" )->getArticleId() ); + Title::newFromText( "$name/error" )->getArticleID() ); $this->assertSame( $ids["$name 2/error"], - Title::newFromText( "$name 2/error" )->getArticleId() ); + Title::newFromText( "$name 2/error" )->getArticleID() ); $results = array_merge( $res[0]['move']['subpages'], $res[0]['move']['subpages-talk'] ); foreach ( $results as $arr ) { @@ -317,7 +351,7 @@ class ApiMoveTest extends ApiTestCase { 'to' => "$name 2", ], null, $user ); } finally { - $this->assertSame( $id, Title::newFromText( "$name" )->getArticleId() ); + $this->assertSame( $id, Title::newFromText( "$name" )->getArticleID() ); $this->assertFalse( Title::newFromText( "$name 2" )->exists(), "\"$name 2\" should not exist" ); } @@ -372,7 +406,7 @@ class ApiMoveTest extends ApiTestCase { ] ); $this->assertMoved( "Talk:$name", $name, $idBase ); - $this->assertSame( $idSub, Title::newFromText( "Talk:$name/1" )->getArticleId() ); + $this->assertSame( $idSub, Title::newFromText( "Talk:$name/1" )->getArticleID() ); $this->assertFalse( Title::newFromText( "$name/1" )->exists(), "\"$name/1\" should not exist" ); diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php index b20d43e28c..f8399a3f33 100644 --- a/tests/phpunit/includes/api/ApiParseTest.php +++ b/tests/phpunit/includes/api/ApiParseTest.php @@ -650,7 +650,6 @@ class ApiParseTest extends ApiTestCase { function ( $parser ) { $output = $parser->getOutput(); $output->addModules( [ 'foo', 'bar' ] ); - $output->addModuleScripts( [ 'baz', 'quuz' ] ); $output->addModuleStyles( [ 'aaa', 'zzz' ] ); $output->addJsConfigVars( [ 'x' => 'y', 'z' => -3 ] ); } @@ -663,7 +662,7 @@ class ApiParseTest extends ApiTestCase { ] ); $this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] ); - $this->assertSame( [ 'baz', 'quuz' ], $res[0]['parse']['modulescripts'] ); + $this->assertSame( [], $res[0]['parse']['modulescripts'] ); $this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] ); $this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] ); $this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] ); diff --git a/tests/phpunit/includes/api/ApiQueryBlocksTest.php b/tests/phpunit/includes/api/ApiQueryBlocksTest.php index 03198a8781..6e0084276f 100644 --- a/tests/phpunit/includes/api/ApiQueryBlocksTest.php +++ b/tests/phpunit/includes/api/ApiQueryBlocksTest.php @@ -112,6 +112,12 @@ class ApiQueryBlocksTest extends ApiTestCase { 'ir_type' => PageRestriction::TYPE_ID, 'ir_value' => $pageId, ] ); + // Page that has been deleted. + $this->db->insert( 'ipblocks_restrictions', [ + 'ir_ipb_id' => $block->getId(), + 'ir_type' => PageRestriction::TYPE_ID, + 'ir_value' => 999999, + ] ); $this->db->insert( 'ipblocks_restrictions', [ 'ir_ipb_id' => $block->getId(), 'ir_type' => NamespaceRestriction::TYPE_ID, diff --git a/tests/phpunit/includes/api/ApiQuerySiteinfoTest.php b/tests/phpunit/includes/api/ApiQuerySiteinfoTest.php index 225c19537b..d3a4ed4406 100644 --- a/tests/phpunit/includes/api/ApiQuerySiteinfoTest.php +++ b/tests/phpunit/includes/api/ApiQuerySiteinfoTest.php @@ -40,36 +40,34 @@ class ApiQuerySiteinfoTest extends ApiTestCase { } public function testLinkPrefixCharset() { - global $wgContLang; - - $this->setContentLang( 'ar' ); - $this->assertTrue( $wgContLang->linkPrefixExtension(), 'Sanity check' ); + $contLang = Language::factory( 'ar' ); + $this->setContentLang( $contLang ); + $this->assertTrue( $contLang->linkPrefixExtension(), 'Sanity check' ); $data = $this->doQuery(); - $this->assertSame( $wgContLang->linkPrefixCharset(), $data['linkprefixcharset'] ); + $this->assertSame( $contLang->linkPrefixCharset(), $data['linkprefixcharset'] ); } public function testVariants() { - global $wgContLang; - - $this->setContentLang( 'zh' ); - $this->assertTrue( $wgContLang->hasVariants(), 'Sanity check' ); + $contLang = Language::factory( 'zh' ); + $this->setContentLang( $contLang ); + $this->assertTrue( $contLang->hasVariants(), 'Sanity check' ); $data = $this->doQuery(); $expected = array_map( - function ( $code ) use ( $wgContLang ) { - return [ 'code' => $code, 'name' => $wgContLang->getVariantname( $code ) ]; + function ( $code ) use ( $contLang ) { + return [ 'code' => $code, 'name' => $contLang->getVariantname( $code ) ]; }, - $wgContLang->getVariants() + $contLang->getVariants() ); $this->assertSame( $expected, $data['variants'] ); } public function testReadOnly() { - $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode(); + $svc = MediaWikiServices::getInstance()->getReadOnlyMode(); $svc->setReason( 'Need more donations' ); try { $data = $this->doQuery(); @@ -82,18 +80,21 @@ class ApiQuerySiteinfoTest extends ApiTestCase { } public function testNamespaces() { - global $wgContLang; - $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] ); - $this->assertSame( array_keys( $wgContLang->getFormattedNamespaces() ), - array_keys( $this->doQuery( 'namespaces' ) ) ); + $this->assertSame( + array_keys( MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces() ), + array_keys( $this->doQuery( 'namespaces' ) ) + ); } public function testNamespaceAliases() { - global $wgNamespaceAliases, $wgContLang; + global $wgNamespaceAliases; - $expected = array_merge( $wgNamespaceAliases, $wgContLang->getNamespaceAliases() ); + $expected = array_merge( + $wgNamespaceAliases, + MediaWikiServices::getInstance()->getContentLanguage()->getNamespaceAliases() + ); $expected = array_map( function ( $key, $val ) { return [ 'id' => $val, 'alias' => strtr( $key, '_', ' ' ) ]; @@ -116,10 +117,8 @@ class ApiQuerySiteinfoTest extends ApiTestCase { } public function testMagicWords() { - global $wgContLang; - $this->assertCount( - count( $wgContLang->getMagicWords() ), + count( MediaWikiServices::getInstance()->getContentLanguage()->getMagicWords() ), $this->doQuery( 'magicwords' ) ); } diff --git a/tests/phpunit/includes/api/ApiStashEditTest.php b/tests/phpunit/includes/api/ApiStashEditTest.php index 5ef3b04e3c..a63f8aa1dc 100644 --- a/tests/phpunit/includes/api/ApiStashEditTest.php +++ b/tests/phpunit/includes/api/ApiStashEditTest.php @@ -331,6 +331,8 @@ class ApiStashEditTest extends ApiTestCase { $cache = ObjectCache::getLocalClusterInstance(); $editInfo = $cache->get( $key ); + $outputKey = $cache->makeKey( 'stashed-edit-output', $editInfo->outputID ); + $editInfo->output = $cache->get( $outputKey ); $editInfo->output->setCacheTime( wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ) - $howOld - 1 ) ); diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 9a27cf1176..43772070d5 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -26,7 +26,6 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { ]; $this->setMwGlobals( [ - 'wgAuth' => new MediaWiki\Auth\AuthManagerAuthPlugin, 'wgRequest' => new FauxRequest( [] ), 'wgUser' => self::$users['sysop']->getUser(), ] ); diff --git a/tests/phpunit/includes/api/ApiUnblockTest.php b/tests/phpunit/includes/api/ApiUnblockTest.php index 6ebd835cf6..ea39da70b2 100644 --- a/tests/phpunit/includes/api/ApiUnblockTest.php +++ b/tests/phpunit/includes/api/ApiUnblockTest.php @@ -127,8 +127,8 @@ class ApiUnblockTest extends ApiTestCase { __METHOD__, [], [ - 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ], - 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ], + 'change_tag' => [ 'JOIN', 'ct_log_id = log_id' ], + 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ], ] ) ); } diff --git a/tests/phpunit/includes/api/ApiUserrightsTest.php b/tests/phpunit/includes/api/ApiUserrightsTest.php index 8cc021763b..5889f8265a 100644 --- a/tests/phpunit/includes/api/ApiUserrightsTest.php +++ b/tests/phpunit/includes/api/ApiUserrightsTest.php @@ -206,7 +206,7 @@ class ApiUserrightsTest extends ApiTestCase { 'log_title' => strtr( $user->getName(), ' ', '_' ) ], __METHOD__, - [ 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] ] + [ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ] ) ); } diff --git a/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php index e67d405448..49601cb786 100644 --- a/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php +++ b/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php @@ -142,7 +142,7 @@ class AbstractPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCa $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) ); $manager->removeAuthenticationSessionData( null ); - $status = \Status::newGood(); + $status = \Status::newGood( [ 'suggestChangeOnLogin' => true ] ); $status->error( 'testing' ); $providerPriv->setPasswordResetFlag( 'Foo', $status ); $ret = $manager->getAuthenticationSessionData( 'reset-pass' ); diff --git a/tests/phpunit/includes/auth/AuthManagerTest.php b/tests/phpunit/includes/auth/AuthManagerTest.php index e8981ec24f..d5e18797cf 100644 --- a/tests/phpunit/includes/auth/AuthManagerTest.php +++ b/tests/phpunit/includes/auth/AuthManagerTest.php @@ -34,12 +34,6 @@ class AuthManagerTest extends \MediaWikiTestCase { /** @var TestingAccessWrapper */ protected $managerPriv; - protected function setUp() { - parent::setUp(); - - $this->setMwGlobals( [ 'wgAuth' => null ] ); - } - /** * Sets a mock on a hook * @param string $hook @@ -2352,8 +2346,6 @@ class AuthManagerTest extends \MediaWikiTestCase { } public function testAutoAccountCreation() { - global $wgHooks; - // PHPUnit seems to have a bug where it will call the ->with() // callbacks for our hooks again after the test is run (WTF?), which // breaks here because $username no longer matches $user by the end of @@ -2771,15 +2763,10 @@ class AuthManagerTest extends \MediaWikiTestCase { $session->clear(); $username = self::usernameForCreation(); $user = \User::newFromName( $username ); - $this->hook( 'AuthPluginAutoCreate', $this->once() ) - ->with( $callback ); - $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' . - get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' ); $this->hook( 'LocalUserCreated', $this->once() ) ->with( $callback, $this->equalTo( true ) ); $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true ); $this->unhook( 'LocalUserCreated' ); - $this->unhook( 'AuthPluginAutoCreate' ); $this->assertEquals( \Status::newGood(), $ret ); $this->assertNotEquals( 0, $user->getId() ); $this->assertEquals( $username, $user->getName() ); diff --git a/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php deleted file mode 100644 index 44e9799c12..0000000000 --- a/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php +++ /dev/null @@ -1,716 +0,0 @@ -fail( 'Expected exception not thrown' ); - } catch ( \InvalidArgumentException $ex ) { - $this->assertSame( - 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' . - 'makes no sense.', - $ex->getMessage() - ); - } - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - [ new PasswordAuthenticationRequest ], - $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] ) - ); - - $req = $this->createMock( PasswordAuthenticationRequest::class ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin, get_class( $req ) ); - $this->assertEquals( - [ $req ], - $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] ) - ); - - $reqType = get_class( $this->createMock( AuthenticationRequest::class ) ); - try { - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin, $reqType ); - $this->fail( 'Expected exception not thrown' ); - } catch ( \InvalidArgumentException $ex ) { - $this->assertSame( - "$reqType is not a MediaWiki\\Auth\\PasswordAuthenticationRequest", - $ex->getMessage() - ); - } - } - - public function testOnUserSaveSettings() { - $user = \User::newFromName( 'UTSysop' ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'updateExternalDB' ) - ->with( $this->identicalTo( $user ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - \Hooks::run( 'UserSaveSettings', [ $user ] ); - } - - public function testOnUserGroupsChanged() { - $user = \User::newFromName( 'UTSysop' ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'updateExternalDBGroups' ) - ->with( - $this->identicalTo( $user ), - $this->identicalTo( [ 'added' ] ), - $this->identicalTo( [ 'removed' ] ) - ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ], false, false, [], [] ] ); - } - - public function testOnUserLoggedIn() { - $user = \User::newFromName( 'UTSysop' ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->exactly( 2 ) )->method( 'updateUser' ) - ->with( $this->identicalTo( $user ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - \Hooks::run( 'UserLoggedIn', [ $user ] ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'updateUser' ) - ->will( $this->returnCallback( function ( &$user ) { - $user = \User::newFromName( 'UTSysop' ); - } ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - try { - \Hooks::run( 'UserLoggedIn', [ $user ] ); - $this->fail( 'Expected exception not thrown' ); - } catch ( \UnexpectedValueException $ex ) { - $this->assertSame( - get_class( $plugin ) . '::updateUser() tried to replace $user!', - $ex->getMessage() - ); - } - } - - public function testOnLocalUserCreated() { - $user = \User::newFromName( 'UTSysop' ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->exactly( 2 ) )->method( 'initUser' ) - ->with( $this->identicalTo( $user ), $this->identicalTo( false ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - \Hooks::run( 'LocalUserCreated', [ $user, false ] ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'initUser' ) - ->will( $this->returnCallback( function ( &$user ) { - $user = \User::newFromName( 'UTSysop' ); - } ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - try { - \Hooks::run( 'LocalUserCreated', [ $user, false ] ); - $this->fail( 'Expected exception not thrown' ); - } catch ( \UnexpectedValueException $ex ) { - $this->assertSame( - get_class( $plugin ) . '::initUser() tried to replace $user!', - $ex->getMessage() - ); - } - } - - public function testGetUniqueId() { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertSame( - 'MediaWiki\\Auth\\AuthPluginPrimaryAuthenticationProvider:' . get_class( $plugin ), - $provider->getUniqueId() - ); - } - - /** - * @dataProvider provideGetAuthenticationRequests - * @param string $action - * @param array $response - * @param bool $allowPasswordChange - */ - public function testGetAuthenticationRequests( $action, $response, $allowPasswordChange ) { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->any() )->method( 'allowPasswordChange' ) - ->will( $this->returnValue( $allowPasswordChange ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) ); - } - - public static function provideGetAuthenticationRequests() { - $arr = [ new PasswordAuthenticationRequest() ]; - return [ - [ AuthManager::ACTION_LOGIN, $arr, true ], - [ AuthManager::ACTION_LOGIN, $arr, false ], - [ AuthManager::ACTION_CREATE, $arr, true ], - [ AuthManager::ACTION_CREATE, $arr, false ], - [ AuthManager::ACTION_LINK, [], true ], - [ AuthManager::ACTION_LINK, [], false ], - [ AuthManager::ACTION_CHANGE, $arr, true ], - [ AuthManager::ACTION_CHANGE, [], false ], - [ AuthManager::ACTION_REMOVE, $arr, true ], - [ AuthManager::ACTION_REMOVE, [], false ], - ]; - } - - public function testAuthentication() { - $req = new PasswordAuthenticationRequest(); - $req->action = AuthManager::ACTION_LOGIN; - $reqs = [ PasswordAuthenticationRequest::class => $req ]; - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'authenticate' ] ) - ->getMock(); - $plugin->expects( $this->never() )->method( 'authenticate' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAuthentication( [] ) - ); - - $req->username = 'foo'; - $req->password = null; - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAuthentication( $reqs ) - ); - - $req->username = null; - $req->password = 'bar'; - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAuthentication( $reqs ) - ); - - $req->username = 'foo'; - $req->password = 'bar'; - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'authenticate' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'authenticate' ) - ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - AuthenticationResponse::newPass( 'Foo', $req ), - $provider->beginPrimaryAuthentication( $reqs ) - ); - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'authenticate' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( false ) ); - $plugin->expects( $this->never() )->method( 'authenticate' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAuthentication( $reqs ) - ); - - $pluginUser = $this->getMockBuilder( \AuthPluginUser::class ) - ->setMethods( [ 'isLocked' ] ) - ->disableOriginalConstructor() - ->getMock(); - $pluginUser->expects( $this->once() )->method( 'isLocked' ) - ->will( $this->returnValue( true ) ); - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'getUserInstance', 'authenticate' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'getUserInstance' ) - ->will( $this->returnValue( $pluginUser ) ); - $plugin->expects( $this->never() )->method( 'authenticate' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAuthentication( $reqs ) - ); - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'authenticate' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'authenticate' ) - ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( false ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAuthentication( $reqs ) - ); - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'authenticate', 'strict' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'authenticate' ) - ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( false ) ); - $plugin->expects( $this->any() )->method( 'strict' )->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $ret = $provider->beginPrimaryAuthentication( $reqs ); - $this->assertSame( AuthenticationResponse::FAIL, $ret->status ); - $this->assertSame( 'wrongpassword', $ret->message->getKey() ); - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'authenticate', 'strictUserAuth' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'authenticate' ) - ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( false ) ); - $plugin->expects( $this->any() )->method( 'strictUserAuth' ) - ->with( $this->equalTo( 'Foo' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $ret = $provider->beginPrimaryAuthentication( $reqs ); - $this->assertSame( AuthenticationResponse::FAIL, $ret->status ); - $this->assertSame( 'wrongpassword', $ret->message->getKey() ); - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'domainList', 'validDomain', 'setDomain', 'userExists', 'authenticate' ] ) - ->getMock(); - $plugin->expects( $this->any() )->method( 'domainList' ) - ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) ); - $plugin->expects( $this->any() )->method( 'validDomain' ) - ->will( $this->returnCallback( function ( $domain ) { - return in_array( $domain, [ 'Domain1', 'Domain2' ] ); - } ) ); - $plugin->expects( $this->once() )->method( 'setDomain' ) - ->with( $this->equalTo( 'Domain2' ) ); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'authenticate' ) - ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] ); - $req->username = 'foo'; - $req->password = 'bar'; - $req->domain = 'Domain2'; - $provider->beginPrimaryAuthentication( [ $req ] ); - } - - public function testTestUserExists() { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->with( $this->equalTo( 'Foo' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - $this->assertTrue( $provider->testUserExists( 'foo' ) ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->with( $this->equalTo( 'Foo' ) ) - ->will( $this->returnValue( false ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - $this->assertFalse( $provider->testUserExists( 'foo' ) ); - } - - public function testTestUserCanAuthenticate() { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->with( $this->equalTo( 'Foo' ) ) - ->will( $this->returnValue( false ) ); - $plugin->expects( $this->never() )->method( 'getUserInstance' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) ); - - $pluginUser = $this->getMockBuilder( \AuthPluginUser::class ) - ->disableOriginalConstructor() - ->getMock(); - $pluginUser->expects( $this->once() )->method( 'isLocked' ) - ->will( $this->returnValue( true ) ); - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->with( $this->equalTo( 'Foo' ) ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'getUserInstance' ) - ->with( $this->callback( function ( $user ) { - $this->assertInstanceOf( \User::class, $user ); - $this->assertEquals( 'Foo', $user->getName() ); - return true; - } ) ) - ->will( $this->returnValue( $pluginUser ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) ); - - $pluginUser = $this->getMockBuilder( \AuthPluginUser::class ) - ->disableOriginalConstructor() - ->getMock(); - $pluginUser->expects( $this->once() )->method( 'isLocked' ) - ->will( $this->returnValue( false ) ); - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'userExists' ) - ->with( $this->equalTo( 'Foo' ) ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'getUserInstance' ) - ->with( $this->callback( function ( $user ) { - $this->assertInstanceOf( \User::class, $user ); - $this->assertEquals( 'Foo', $user->getName() ); - return true; - } ) ) - ->will( $this->returnValue( $pluginUser ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertTrue( $provider->testUserCanAuthenticate( 'foo' ) ); - } - - public function testProviderRevokeAccessForUser() { - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'userExists', 'setPassword' ] ) - ->getMock(); - $plugin->expects( $this->once() )->method( 'userExists' )->willReturn( true ); - $plugin->expects( $this->once() )->method( 'setPassword' ) - ->with( $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), $this->identicalTo( null ) ) - ->willReturn( true ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $provider->providerRevokeAccessForUser( 'foo' ); - - $plugin = $this->getMockBuilder( \AuthPlugin::class ) - ->setMethods( [ 'domainList', 'userExists', 'setPassword' ] ) - ->getMock(); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [ 'D1', 'D2', 'D3' ] ); - $plugin->expects( $this->exactly( 3 ) )->method( 'userExists' ) - ->willReturnCallback( function () use ( $plugin ) { - return $plugin->getDomain() !== 'D2'; - } ); - $plugin->expects( $this->exactly( 2 ) )->method( 'setPassword' ) - ->with( $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), $this->identicalTo( null ) ) - ->willReturnCallback( function () use ( $plugin ) { - $this->assertNotEquals( 'D2', $plugin->getDomain() ); - return $plugin->getDomain() !== 'D1'; - } ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - try { - $provider->providerRevokeAccessForUser( 'foo' ); - $this->fail( 'Expected exception not thrown' ); - } catch ( \UnexpectedValueException $ex ) { - $this->assertSame( - 'AuthPlugin failed to reset password for Foo in the following domains: D1', - $ex->getMessage() - ); - } - } - - public function testProviderAllowsPropertyChange() { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->any() )->method( 'allowPropChange' ) - ->will( $this->returnCallback( function ( $prop ) { - return $prop === 'allow'; - } ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - $this->assertTrue( $provider->providerAllowsPropertyChange( 'allow' ) ); - $this->assertFalse( $provider->providerAllowsPropertyChange( 'deny' ) ); - } - - /** - * @dataProvider provideProviderAllowsAuthenticationDataChange - * @param string $type - * @param bool|null $allow - * @param StatusValue $expect - */ - public function testProviderAllowsAuthenticationDataChange( $type, $allow, $expect ) { - $domains = $type instanceof PasswordDomainAuthenticationRequest ? [ 'foo', 'bar' ] : []; - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( $domains ); - $plugin->expects( $allow === null ? $this->never() : $this->once() ) - ->method( 'allowPasswordChange' )->will( $this->returnValue( $allow ) ); - $plugin->expects( $this->any() )->method( 'validDomain' ) - ->willReturnCallback( function ( $d ) use ( $domains ) { - return in_array( $d, $domains, true ); - } ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - if ( is_object( $type ) ) { - $req = $type; - } else { - $req = $this->createMock( $type ); - } - $req->action = AuthManager::ACTION_CHANGE; - $req->username = 'UTSysop'; - $req->password = 'Pa$$w0Rd!!!'; - $req->retype = 'Pa$$w0Rd!!!'; - $this->assertEquals( $expect, $provider->providerAllowsAuthenticationDataChange( $req ) ); - } - - public static function provideProviderAllowsAuthenticationDataChange() { - $domains = [ 'foo', 'bar' ]; - $reqNoDomain = new PasswordDomainAuthenticationRequest( $domains ); - $reqValidDomain = new PasswordDomainAuthenticationRequest( $domains ); - $reqValidDomain->domain = 'foo'; - $reqInvalidDomain = new PasswordDomainAuthenticationRequest( $domains ); - $reqInvalidDomain->domain = 'invalid'; - - return [ - [ AuthenticationRequest::class, null, \StatusValue::newGood( 'ignored' ) ], - [ new PasswordAuthenticationRequest, true, \StatusValue::newGood() ], - [ - new PasswordAuthenticationRequest, - false, - \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' ) - ], - [ $reqNoDomain, true, \StatusValue::newGood( 'ignored' ) ], - [ $reqValidDomain, true, \StatusValue::newGood() ], - [ - $reqInvalidDomain, - true, - \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' ) - ], - ]; - } - - public function testProviderChangeAuthenticationData() { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->never() )->method( 'setPassword' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $provider->providerChangeAuthenticationData( - $this->createMock( AuthenticationRequest::class ) - ); - - $req = new PasswordAuthenticationRequest(); - $req->action = AuthManager::ACTION_CHANGE; - $req->username = 'foo'; - $req->password = 'bar'; - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'setPassword' ) - ->with( $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $provider->providerChangeAuthenticationData( $req ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() )->method( 'setPassword' ) - ->with( $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( false ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - try { - $provider->providerChangeAuthenticationData( $req ); - $this->fail( 'Expected exception not thrown' ); - } catch ( \ErrorPageError $e ) { - $this->assertSame( 'authmanager-authplugin-setpass-failed-title', $e->title ); - $this->assertSame( 'authmanager-authplugin-setpass-failed-message', $e->msg ); - } - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' ) - ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) ); - $plugin->expects( $this->any() )->method( 'validDomain' ) - ->will( $this->returnCallback( function ( $domain ) { - return in_array( $domain, [ 'Domain1', 'Domain2' ] ); - } ) ); - $plugin->expects( $this->once() )->method( 'setDomain' ) - ->with( $this->equalTo( 'Domain2' ) ); - $plugin->expects( $this->once() )->method( 'setPassword' ) - ->with( $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE, [] ); - $req->username = 'foo'; - $req->password = 'bar'; - $req->domain = 'Domain2'; - $provider->providerChangeAuthenticationData( $req ); - } - - /** - * @dataProvider provideAccountCreationType - * @param bool $can - * @param string $expect - */ - public function testAccountCreationType( $can, $expect ) { - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->once() ) - ->method( 'canCreateAccounts' )->will( $this->returnValue( $can ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - $this->assertSame( $expect, $provider->accountCreationType() ); - } - - public static function provideAccountCreationType() { - return [ - [ true, PrimaryAuthenticationProvider::TYPE_CREATE ], - [ false, PrimaryAuthenticationProvider::TYPE_NONE ], - ]; - } - - public function testTestForAccountCreation() { - $user = \User::newFromName( 'foo' ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - \StatusValue::newGood(), - $provider->testForAccountCreation( $user, $user, [] ) - ); - } - - public function testAccountCreation() { - $user = \User::newFromName( 'foo' ); - $user->setEmail( 'email' ); - $user->setRealName( 'realname' ); - - $req = new PasswordAuthenticationRequest(); - $req->action = AuthManager::ACTION_CREATE; - $reqs = [ PasswordAuthenticationRequest::class => $req ]; - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) - ->will( $this->returnValue( false ) ); - $plugin->expects( $this->never() )->method( 'addUser' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - try { - $provider->beginPrimaryAccountCreation( $user, $user, [] ); - $this->fail( 'Expected exception was not thrown' ); - } catch ( \BadMethodCallException $ex ) { - $this->assertSame( - 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage() - ); - } - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->never() )->method( 'addUser' ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAccountCreation( $user, $user, [] ) - ); - - $req->username = 'foo'; - $req->password = null; - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) - ); - - $req->username = null; - $req->password = 'bar'; - $this->assertEquals( - AuthenticationResponse::newAbstain(), - $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) - ); - - $req->username = 'foo'; - $req->password = 'bar'; - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'addUser' ) - ->with( - $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), - $this->equalTo( 'bar' ), - $this->equalTo( 'email' ), - $this->equalTo( 'realname' ) - ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $this->assertEquals( - AuthenticationResponse::newPass(), - $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) - ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); - $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->once() )->method( 'addUser' ) - ->with( - $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), - $this->equalTo( 'bar' ), - $this->equalTo( 'email' ), - $this->equalTo( 'realname' ) - ) - ->will( $this->returnValue( false ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - $ret = $provider->beginPrimaryAccountCreation( $user, $user, $reqs ); - $this->assertSame( AuthenticationResponse::FAIL, $ret->status ); - $this->assertSame( 'authmanager-authplugin-create-fail', $ret->message->getKey() ); - - $plugin = $this->createMock( \AuthPlugin::class ); - $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) - ->will( $this->returnValue( true ) ); - $plugin->expects( $this->any() )->method( 'domainList' ) - ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) ); - $plugin->expects( $this->any() )->method( 'validDomain' ) - ->will( $this->returnCallback( function ( $domain ) { - return in_array( $domain, [ 'Domain1', 'Domain2' ] ); - } ) ); - $plugin->expects( $this->once() )->method( 'setDomain' ) - ->with( $this->equalTo( 'Domain2' ) ); - $plugin->expects( $this->once() )->method( 'addUser' ) - ->with( $this->callback( function ( $u ) { - return $u instanceof \User && $u->getName() === 'Foo'; - } ), $this->equalTo( 'bar' ) ) - ->will( $this->returnValue( true ) ); - $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE, [] ); - $req->username = 'foo'; - $req->password = 'bar'; - $req->domain = 'Domain2'; - $provider->beginPrimaryAccountCreation( $user, $user, [ $req ] ); - } - -} diff --git a/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php index bbc1192679..14d7f09b96 100644 --- a/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php +++ b/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php @@ -165,6 +165,7 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase $user->saveSettings(); } $this->setMwGlobals( [ 'wgUser' => $user ] ); + \RequestContext::getMain()->setUser( $user ); $newuser = \User::newFromName( 'RandomUser' ); $provider = new CheckBlocksSecondaryAuthenticationProvider( diff --git a/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php index e1b25a1ebd..6d831f6a0a 100644 --- a/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php +++ b/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php @@ -38,11 +38,11 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase $this->manager = new AuthManager( new \FauxRequest(), $config ); } $this->validity = \Status::newGood(); - $provider = $this->getMockBuilder( LocalPasswordPrimaryAuthenticationProvider::class ) ->setMethods( [ 'checkPasswordValidity' ] ) ->setConstructorArgs( [ [ 'loginOnly' => $loginOnly ] ] ) ->getMock(); + $provider->expects( $this->any() )->method( 'checkPasswordValidity' ) ->will( $this->returnCallback( function () { return $this->validity; @@ -167,7 +167,7 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase $this->manager->removeAuthenticationSessionData( null ); $row->user_password_expires = null; - $status = \Status::newGood(); + $status = \Status::newGood( [ 'suggestChangeOnLogin' => true ] ); $status->error( 'testing' ); $providerPriv->setPasswordResetFlag( $userName, $status, $row ); $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' ); @@ -184,6 +184,14 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase $this->assertNotNull( $ret ); $this->assertSame( 'resetpass-validity', $ret->msg->getKey() ); $this->assertTrue( $ret->hard ); + + $this->manager->removeAuthenticationSessionData( null ); + $row->user_password_expires = null; + $status = \Status::newGood( [ 'suggestChangeOnLogin' => false, ] ); + $status->error( 'testing' ); + $providerPriv->setPasswordResetFlag( $userName, $status, $row ); + $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' ); + $this->assertNull( $ret ); } public function testAuthentication() { @@ -275,6 +283,7 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase // Successful auth with reset $this->manager->removeAuthenticationSessionData( null ); + $this->validity = \Status::newGood( [ 'suggestChangeOnLogin' => true ] ); $this->validity->error( 'arbitrary-warning' ); $this->assertEquals( AuthenticationResponse::newPass( $userName ), @@ -664,5 +673,4 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase $ret = $provider->beginPrimaryAuthentication( $reqs ); $this->assertEquals( AuthenticationResponse::PASS, $ret->status, 'new password is set' ); } - } diff --git a/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php b/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php index dc4ab6f497..1ee4a039d9 100644 --- a/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php +++ b/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php @@ -26,18 +26,32 @@ class TemporaryPasswordAuthenticationRequestTest extends AuthenticationRequestTe global $wgPasswordPolicy; $policy = $wgPasswordPolicy; - $policy['policies']['default'] += [ + unset( $policy['policies'] ); + $policy['policies']['default'] = [ 'MinimalPasswordLength' => 1, - 'MinimalPasswordLengthToLogin' => 1, + 'MinimumPasswordLengthToLogin' => 1, ]; - $this->setMwGlobals( 'wgPasswordPolicy', $policy ); + $this->setMwGlobals( [ + 'wgMinimalPasswordLength' => 10, + 'wgPasswordPolicy' => $policy, + ] ); $ret1 = TemporaryPasswordAuthenticationRequest::newRandom(); $ret2 = TemporaryPasswordAuthenticationRequest::newRandom(); - $this->assertNotSame( '', $ret1->password ); - $this->assertNotSame( '', $ret2->password ); + $this->assertEquals( 10, strlen( $ret1->password ) ); + $this->assertEquals( 10, strlen( $ret2->password ) ); $this->assertNotSame( $ret1->password, $ret2->password ); + + $policy['policies']['default']['MinimalPasswordLength'] = 15; + $this->setMwGlobals( 'wgPasswordPolicy', $policy ); + $ret = TemporaryPasswordAuthenticationRequest::newRandom(); + $this->assertEquals( 15, strlen( $ret->password ) ); + + $policy['policies']['default']['MinimalPasswordLength'] = [ 'value' => 20 ]; + $this->setMwGlobals( 'wgPasswordPolicy', $policy ); + $ret = TemporaryPasswordAuthenticationRequest::newRandom(); + $this->assertEquals( 20, strlen( $ret->password ) ); } public function testNewInvalid() { diff --git a/tests/phpunit/includes/block/BlockRestrictionTest.php b/tests/phpunit/includes/block/BlockRestrictionTest.php index 2d78018c4a..5bbd3d023d 100644 --- a/tests/phpunit/includes/block/BlockRestrictionTest.php +++ b/tests/phpunit/includes/block/BlockRestrictionTest.php @@ -25,6 +25,9 @@ class BlockRestrictionTest extends \MediaWikiLangTestCase { * @covers ::rowToRestriction */ public function testLoadMultipleRestrictions() { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $block = $this->insertBlock(); $pageFoo = $this->getExistingTestPage( 'Foo' ); diff --git a/tests/phpunit/includes/block/Restriction/NamespaceRestrictionTest.php b/tests/phpunit/includes/block/Restriction/NamespaceRestrictionTest.php index 4356240f20..8f54789fec 100644 --- a/tests/phpunit/includes/block/Restriction/NamespaceRestrictionTest.php +++ b/tests/phpunit/includes/block/Restriction/NamespaceRestrictionTest.php @@ -29,7 +29,7 @@ class NamespaceRestrictionTest extends RestrictionTestCase { } /** - * {@inheritdoc} + * @inheritDoc */ protected function getClass() { return NamespaceRestriction::class; diff --git a/tests/phpunit/includes/block/Restriction/PageRestrictionTest.php b/tests/phpunit/includes/block/Restriction/PageRestrictionTest.php index 95cb3b7b72..c547878760 100644 --- a/tests/phpunit/includes/block/Restriction/PageRestrictionTest.php +++ b/tests/phpunit/includes/block/Restriction/PageRestrictionTest.php @@ -20,6 +20,11 @@ class PageRestrictionTest extends RestrictionTestCase { $page = $this->getExistingTestPage( 'Mars' ); $this->assertFalse( $restriction->matches( $page->getTitle() ) ); + + // Deleted page. + $restriction = new $class( 2, 99999 ); + $page = $this->getExistingTestPage( 'Saturn' ); + $this->assertFalse( $restriction->matches( $page->getTitle() ) ); } public function testGetType() { @@ -38,7 +43,7 @@ class PageRestrictionTest extends RestrictionTestCase { $restriction = new $class( 1, 1 ); $title = \Title::newFromId( 1 ); - $this->assertEquals( $title->getArticleId(), $restriction->getTitle()->getArticleId() ); + $this->assertEquals( $title->getArticleID(), $restriction->getTitle()->getArticleID() ); } public function testNewFromRow() { @@ -56,7 +61,7 @@ class PageRestrictionTest extends RestrictionTestCase { } /** - * {@inheritdoc} + * @inheritDoc */ protected function getClass() { return PageRestriction::class; diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php index c340c08e49..b03a3098d1 100644 --- a/tests/phpunit/includes/cache/MessageCacheTest.php +++ b/tests/phpunit/includes/cache/MessageCacheTest.php @@ -1,6 +1,7 @@ doEditContent( $contentHandler, "$lang translation test case" ); + $status = $wikiPage->doEditContent( $contentHandler, "$lang translation test case" ); + + // sanity + $this->assertTrue( $status->isOK(), 'Create page ' . $title->getPrefixedDBkey() ); + return $status->value['revision']; } /** @@ -213,4 +220,42 @@ class MessageCacheTest extends MediaWikiLangTestCase { $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries" ); } + + /** + * Regression test for T218918 + */ + public function testLoadFromDB_fetchLatestRevision() { + // Create three revisions of the same message page. + // Must be an existing message key. + $key = 'Log'; + $this->makePage( $key, 'de', 'Test eins' ); + $this->makePage( $key, 'de', 'Test zwei' ); + $r3 = $this->makePage( $key, 'de', 'Test drei' ); + + // Create an out-of-sequence revision by importing a + // revision with an old timestamp. Hacky. + $importRevision = new WikiRevision( new HashConfig() ); + $importRevision->setTitle( $r3->getTitle() ); + $importRevision->setComment( 'Imported edit' ); + $importRevision->setTimestamp( '19991122001122' ); + $importRevision->setText( 'IMPORTED OLD TEST' ); + $importRevision->setUsername( 'Alan Smithee' ); + + $importer = MediaWikiServices::getInstance()->getWikiRevisionOldRevisionImporterNoUpdates(); + $importer->import( $importRevision ); + + // Now, load the message from the wiki page + MessageCache::destroyInstance(); + $messageCache = MessageCache::singleton(); + $messageCache->enable(); + $messageCache = TestingAccessWrapper::newFromObject( $messageCache ); + + $cache = $messageCache->loadFromDB( 'de' ); + + $this->assertArrayHasKey( $key, $cache ); + + // Text in the cache has an extra space in front! + $this->assertSame( ' ' . 'Test drei', $cache[$key] ); + } + } diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php index 420fe7493e..1511d46c5c 100644 --- a/tests/phpunit/includes/changes/EnhancedChangesListTest.php +++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php @@ -26,6 +26,12 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $styleModules = $enhancedChangesList->getOutput()->getModuleStyles(); + $this->assertContains( + 'mediawiki.icon', + $styleModules, + 'has mediawiki.icon' + ); + $this->assertContains( 'mediawiki.special.changeslist', $styleModules, @@ -46,7 +52,6 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $modules = $enhancedChangesList->getOutput()->getModules(); $this->assertContains( 'jquery.makeCollapsible', $modules, 'has jquery.makeCollapsible' ); - $this->assertContains( 'mediawiki.icon', $modules, 'has mediawiki.icon' ); } public function testBeginRecentChangesList_html() { @@ -119,14 +124,14 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $html = $this->createCategorizationLine( $this->getCategorizationChange( '20150629191735', 0, 0 ) ); - $this->assertNotContains( '(diff | hist)', strip_tags( $html ) ); + $this->assertNotContains( 'diffhist', strip_tags( $html ) ); } public function testCategorizationLineFormattingWithRevision() { $html = $this->createCategorizationLine( $this->getCategorizationChange( '20150629191735', 1025, 1024 ) ); - $this->assertContains( '(diff | hist)', strip_tags( $html ) ); + $this->assertContains( 'diffhist', strip_tags( $html ) ); } /** diff --git a/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php b/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php index b1857cccf0..8f914b714a 100644 --- a/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php +++ b/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php @@ -156,14 +156,15 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase { $this->assertValidHTML( $cacheEntry->usertalklink ); $this->assertRegExp( - '#^ \(.*talk.*\)#', + '#^ .*talk.*#', $cacheEntry->usertalklink, 'verify user talk link' ); $this->assertValidHTML( $cacheEntry->usertalklink ); $this->assertRegExp( - '#^ \(.*contribs.*\)$#', + '#^ .*' . + 'contribs.*$#', $cacheEntry->usertalklink, 'verify user tool links' ); diff --git a/tests/phpunit/includes/changetags/ChangeTagsTest.php b/tests/phpunit/includes/changetags/ChangeTagsTest.php index e9058b620d..1405680b6f 100644 --- a/tests/phpunit/includes/changetags/ChangeTagsTest.php +++ b/tests/phpunit/includes/changetags/ChangeTagsTest.php @@ -62,7 +62,7 @@ class ChangeTagsTest extends MediaWikiTestCase { // HACK if we call $dbr->buildGroupConcatField() now, it will return the wrong table names // We have to have the test runner call it instead $baseConcats = [ ',', [ 'change_tag', 'change_tag_def' ], 'ctd_name' ]; - $joinConds = [ 'change_tag_def' => [ 'INNER JOIN', 'ct_tag_id=ctd_id' ] ]; + $joinConds = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ]; $groupConcats = [ 'recentchanges' => array_merge( $baseConcats, [ 'ct_rc_id=rc_id', $joinConds ] ), 'logging' => array_merge( $baseConcats, [ 'ct_log_id=log_id', $joinConds ] ), @@ -121,7 +121,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'recentchanges', 'change_tag' ], 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ], 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ], 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ], ] ], @@ -139,7 +139,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'logging', 'change_tag' ], 'fields' => [ 'log_id', 'ts_tags' => $groupConcats['logging'] ], 'conds' => [ "log_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id=log_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_log_id=log_id' ] ], 'options' => [ 'ORDER BY log_timestamp DESC' ], ] ], @@ -157,7 +157,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'revision', 'change_tag' ], 'fields' => [ 'rev_id', 'rev_timestamp', 'ts_tags' => $groupConcats['revision'] ], 'conds' => [ "rev_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=rev_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=rev_id' ] ], 'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ], ] ], @@ -175,7 +175,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'archive', 'change_tag' ], 'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ], 'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=ar_rev_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=ar_rev_id' ] ], 'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ], ] ], @@ -223,7 +223,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'recentchanges', 'change_tag' ], 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ], 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ], 'options' => [ 'ORDER BY' => 'rc_timestamp DESC', 'DISTINCT' ], ] ], @@ -241,7 +241,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'recentchanges', 'change_tag' ], 'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ], 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ], 'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ], ] ], @@ -259,7 +259,7 @@ class ChangeTagsTest extends MediaWikiTestCase { 'tables' => [ 'recentchanges', 'change_tag' ], 'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ], 'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ], - 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ], + 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ], 'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ], ] ], @@ -592,7 +592,13 @@ class ChangeTagsTest extends MediaWikiTestCase { 'ctd_user_defined' => 1 ], ]; - $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' ); + $res = $dbr->select( + 'change_tag_def', + [ 'ctd_name', 'ctd_user_defined' ], + '', + __METHOD__, + [ 'ORDER BY' => 'ctd_name' ] + ); $this->assertEquals( $expected, iterator_to_array( $res, false ) ); } } diff --git a/tests/phpunit/includes/content/ContentHandlerTest.php b/tests/phpunit/includes/content/ContentHandlerTest.php index a8ea3f0af3..f42f8b49f9 100644 --- a/tests/phpunit/includes/content/ContentHandlerTest.php +++ b/tests/phpunit/includes/content/ContentHandlerTest.php @@ -158,6 +158,7 @@ class ContentHandlerTest extends MediaWikiTestCase { $handler = ContentHandler::getForTitle( $title ); $lang = $handler->getPageLanguage( $title ); + $this->assertInstanceOf( Language::class, $lang ); $this->assertEquals( $expected->getCode(), $lang->getCode() ); } diff --git a/tests/phpunit/includes/content/MessageContentTest.php b/tests/phpunit/includes/content/MessageContentTest.php index 60f68e76cc..5df7cca444 100644 --- a/tests/phpunit/includes/content/MessageContentTest.php +++ b/tests/phpunit/includes/content/MessageContentTest.php @@ -2,6 +2,7 @@ /** * @group ContentHandler + * @covers MessageContent */ class MessageContentTest extends MediaWikiLangTestCase { diff --git a/tests/phpunit/includes/content/TextContentTest.php b/tests/phpunit/includes/content/TextContentTest.php index 8e537d684a..ecd23f1338 100644 --- a/tests/phpunit/includes/content/TextContentTest.php +++ b/tests/phpunit/includes/content/TextContentTest.php @@ -487,6 +487,10 @@ class TextContentTest extends MediaWikiLangTestCase { ]; } + /** + * @covers TextContent::__construct + * @covers TextContentHandler::serializeContent + */ public function testSerialize() { $cnt = $this->newContent( 'testing text' ); diff --git a/tests/phpunit/includes/content/WikitextContentHandlerTest.php b/tests/phpunit/includes/content/WikitextContentHandlerTest.php index 5f78a5c00c..b372e37c9f 100644 --- a/tests/phpunit/includes/content/WikitextContentHandlerTest.php +++ b/tests/phpunit/includes/content/WikitextContentHandlerTest.php @@ -366,6 +366,9 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase { $this->assertEquals( 'This is file content', $data['file_text'] ); } + /** + * @covers ContentHandler::getSecondaryDataUpdates + */ public function testGetSecondaryDataUpdates() { $title = Title::newFromText( 'Somefile.jpg', NS_FILE ); $content = new WikitextContent( '' ); @@ -379,6 +382,9 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase { $this->assertEquals( [], $updates ); } + /** + * @covers ContentHandler::getDeletionUpdates + */ public function testGetDeletionUpdates() { $title = Title::newFromText( 'Somefile.jpg', NS_FILE ); $content = new WikitextContent( '' ); diff --git a/tests/phpunit/includes/content/WikitextContentTest.php b/tests/phpunit/includes/content/WikitextContentTest.php index f689cae73b..cd7cc1010d 100644 --- a/tests/phpunit/includes/content/WikitextContentTest.php +++ b/tests/phpunit/includes/content/WikitextContentTest.php @@ -182,9 +182,10 @@ just a test" */ public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) { $content = $this->newContent( $text ); + /** @var WikitextContent $c */ $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle ); - $this->assertEquals( $expected, is_null( $c ) ? null : $c->getText() ); + $this->assertEquals( $expected, $c ? $c->getText() : null ); } /** @@ -360,6 +361,10 @@ just a test" $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() ); } + /** + * @covers ParserOptions::getRedirectTarget + * @covers ParserOptions::setRedirectTarget + */ public function testRedirectParserOption() { $title = Title::newFromText( 'testRedirectParserOption' ); diff --git a/tests/phpunit/includes/db/DatabasePostgresTest.php b/tests/phpunit/includes/db/DatabasePostgresTest.php index 6b1ed7fa21..e0b81117f9 100644 --- a/tests/phpunit/includes/db/DatabasePostgresTest.php +++ b/tests/phpunit/includes/db/DatabasePostgresTest.php @@ -1,5 +1,6 @@ doTestInsertSelectIgnore(); } + /** + * @covers \Wikimedia\Rdbms\DatabasePostgres::getAttributes + */ + public function testAttributes() { + $this->assertTrue( DatabasePostgres::getAttributes()[Database::ATTR_SCHEMAS_AS_TABLE_GROUPS] ); + } } diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php index 78af11d25d..63b24dc688 100644 --- a/tests/phpunit/includes/db/DatabaseSqliteTest.php +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -5,26 +5,6 @@ use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DatabaseSqlite; use Wikimedia\Rdbms\ResultWrapper; -class DatabaseSqliteMock extends DatabaseSqlite { - public static function newInstance( array $p = [] ) { - $p['dbFilePath'] = ':memory:'; - $p['schema'] = false; - - return Database::factory( 'SqliteMock', $p ); - } - - function query( $sql, $fname = '', $tempIgnore = false ) { - return true; - } - - /** - * Override parent visibility to public - */ - public function replaceVars( $s ) { - return parent::replaceVars( $s ); - } -} - /** * @group sqlite * @group Database @@ -184,9 +164,9 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $db = DatabaseSqlite::newStandaloneInstance( ':memory:' ); $this->assertEquals( 'foo', $db->tableName( 'foo' ) ); $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) ); - $db->tablePrefix( 'foo' ); + $db->tablePrefix( 'foo_' ); $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) ); - $this->assertEquals( 'foobar', $db->tableName( 'bar' ) ); + $this->assertEquals( 'foo_bar', $db->tableName( 'bar' ) ); } /** @@ -494,6 +474,9 @@ class DatabaseSqliteTest extends MediaWikiTestCase { return $indexes; } + /** + * @coversNothing + */ public function testCaseInsensitiveLike() { // TODO: Test this for all databases $db = DatabaseSqlite::newStandaloneInstance( ':memory:' ); @@ -539,3 +522,23 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $this->assertTrue( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ); } } + +class DatabaseSqliteMock extends DatabaseSqlite { + public static function newInstance( array $p = [] ) { + $p['dbFilePath'] = ':memory:'; + $p['schema'] = false; + + return Database::factory( 'SqliteMock', $p ); + } + + function query( $sql, $fname = '', $flags = 0 ) { + return true; + } + + /** + * Override parent visibility to public + */ + public function replaceVars( $s ) { + return parent::replaceVars( $s ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php index 65b82abf0f..fb4041dd92 100644 --- a/tests/phpunit/includes/db/DatabaseTestHelper.php +++ b/tests/phpunit/includes/db/DatabaseTestHelper.php @@ -45,7 +45,7 @@ class DatabaseTestHelper extends Database { public function __construct( $testName, array $opts = [] ) { $this->testName = $testName; - $this->profiler = new ProfilerStub( [] ); + $this->profiler = null; $this->trxProfiler = new TransactionProfiler(); $this->cliMode = $opts['cliMode'] ?? true; $this->connLogger = new \Psr\Log\NullLogger(); @@ -108,7 +108,11 @@ class DatabaseTestHelper extends Database { // Handle some internal calls from the Database class $check = $fname; - if ( preg_match( '/^Wikimedia\\\\Rdbms\\\\Database::query \((.+)\)$/', $fname, $m ) ) { + if ( preg_match( + '/^Wikimedia\\\\Rdbms\\\\Database::(?:query|beginIfImplied) \((.+)\)$/', + $fname, + $m + ) ) { $check = $m[1]; } @@ -129,10 +133,10 @@ class DatabaseTestHelper extends Database { return $s; } - public function query( $sql, $fname = '', $tempIgnore = false ) { + public function query( $sql, $fname = '', $flags = 0 ) { $this->checkFunctionName( $fname ); - return parent::query( $sql, $fname, $tempIgnore ); + return parent::query( $sql, $fname, $flags ); } public function tableExists( $table, $fname = __METHOD__ ) { diff --git a/tests/phpunit/includes/db/LBFactoryTest.php b/tests/phpunit/includes/db/LBFactoryTest.php index 58f96546cd..7d13ac6ea6 100644 --- a/tests/phpunit/includes/db/LBFactoryTest.php +++ b/tests/phpunit/includes/db/LBFactoryTest.php @@ -23,6 +23,8 @@ * @copyright © 2013 Wikimedia Foundation Inc. */ +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\IMaintainableDatabase; use Wikimedia\Rdbms\LBFactory; use Wikimedia\Rdbms\LBFactorySimple; use Wikimedia\Rdbms\LBFactoryMulti; @@ -435,6 +437,13 @@ class LBFactoryTest extends MediaWikiTestCase { ] ); } + /** + * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::doSelectDomain + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::selectDB + * @covers \Wikimedia\Rdbms\DatabaseMssql::selectDB + * @covers DatabaseOracle::selectDB + */ public function testNiceDomains() { global $wgDBname; @@ -456,7 +465,7 @@ class LBFactoryTest extends MediaWikiTestCase { ); unset( $db ); - /** @var Database $db */ + /** @var IMaintainableDatabase $db */ $db = $lb->getConnection( DB_MASTER, [], '' ); $this->assertEquals( @@ -515,6 +524,13 @@ class LBFactoryTest extends MediaWikiTestCase { $factory->destroy(); } + /** + * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::doSelectDomain + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::selectDB + * @covers \Wikimedia\Rdbms\DatabaseMssql::selectDB + * @covers DatabaseOracle::selectDB + */ public function testTrickyDomain() { global $wgDBname; @@ -527,11 +543,11 @@ class LBFactoryTest extends MediaWikiTestCase { $factory = $this->newLBFactoryMulti( [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ], [ - 'dbName' => 'do_not_select_me' // explodes if DB is selected + 'dbname' => 'do_not_select_me' // explodes if DB is selected ] ); $lb = $factory->getMainLB(); - /** @var Database $db */ + /** @var IMaintainableDatabase $db */ $db = $lb->getConnection( DB_MASTER, [], '' ); $this->assertEquals( '', $db->getDomainID(), "Null domain used" ); @@ -581,46 +597,94 @@ class LBFactoryTest extends MediaWikiTestCase { $factory->destroy(); } + /** + * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::doSelectDomain + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::selectDB + * @covers \Wikimedia\Rdbms\DatabaseMssql::selectDB + * @covers DatabaseOracle::selectDB + */ public function testInvalidSelectDB() { - // FIXME: fails under sqlite - $this->markTestSkippedIfDbType( 'sqlite' ); + if ( wfGetDB( DB_MASTER )->databasesAreIndependent() ) { + $this->markTestSkipped( "Not applicable per databasesAreIndependent()" ); + } + $dbname = 'unittest-domain'; // explodes if DB is selected $factory = $this->newLBFactoryMulti( [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ], [ - 'dbName' => 'do_not_select_me' // explodes if DB is selected + 'dbname' => 'do_not_select_me' // explodes if DB is selected ] ); $lb = $factory->getMainLB(); - /** @var Database $db */ + /** @var IDatabase $db */ $db = $lb->getConnection( DB_MASTER, [], '' ); - if ( $db->getType() === 'sqlite' ) { + \Wikimedia\suppressWarnings(); + try { $this->assertFalse( $db->selectDB( 'garbage-db' ) ); - } elseif ( $db->databasesAreIndependent() ) { - try { - $e = null; - $db->selectDB( 'garbage-db' ); - } catch ( \Wikimedia\Rdbms\DBConnectionError $e ) { - // expected - } - $this->assertInstanceOf( \Wikimedia\Rdbms\DBConnectionError::class, $e ); - $this->assertFalse( $db->isOpen() ); - } else { - \Wikimedia\suppressWarnings(); - try { - $this->assertFalse( $db->selectDB( 'garbage-db' ) ); - $this->fail( "No error thrown." ); - } catch ( \Wikimedia\Rdbms\DBExpectedError $e ) { - $this->assertEquals( - "Could not select database 'garbage-db'.", - $e->getMessage() - ); - } - \Wikimedia\restoreWarnings(); + $this->fail( "No error thrown." ); + } catch ( \Wikimedia\Rdbms\DBQueryError $e ) { + $this->assertRegExp( '/[\'"]garbage-db[\'"]/', $e->getMessage() ); } + \Wikimedia\restoreWarnings(); } + /** + * @covers \Wikimedia\Rdbms\DatabaseSqlite::selectDB + * @covers \Wikimedia\Rdbms\DatabasePostgres::selectDB + * @expectedException \Wikimedia\Rdbms\DBConnectionError + */ + public function testInvalidSelectDBIndependant() { + $dbname = 'unittest-domain'; // explodes if DB is selected + $factory = $this->newLBFactoryMulti( + [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ], + [ + 'dbname' => 'do_not_select_me' // explodes if DB is selected + ] + ); + $lb = $factory->getMainLB(); + + if ( !wfGetDB( DB_MASTER )->databasesAreIndependent() ) { + $this->markTestSkipped( "Not applicable per databasesAreIndependent()" ); + } + + /** @var IDatabase $db */ + $lb->getConnection( DB_MASTER, [], '' ); + } + + /** + * @covers \Wikimedia\Rdbms\DatabaseSqlite::selectDB + * @covers \Wikimedia\Rdbms\DatabasePostgres::selectDB + * @expectedException \Wikimedia\Rdbms\DBConnectionError + */ + public function testInvalidSelectDBIndependant2() { + $dbname = 'unittest-domain'; // explodes if DB is selected + $factory = $this->newLBFactoryMulti( + [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ], + [ + 'dbname' => 'do_not_select_me' // explodes if DB is selected + ] + ); + $lb = $factory->getMainLB(); + + if ( !wfGetDB( DB_MASTER )->databasesAreIndependent() ) { + $this->markTestSkipped( "Not applicable per databasesAreIndependent()" ); + } + + $db = $lb->getConnection( DB_MASTER ); + \Wikimedia\suppressWarnings(); + $db->selectDB( 'garbage-db' ); + \Wikimedia\restoreWarnings(); + } + + /** + * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection + * @covers \Wikimedia\Rdbms\LoadBalancer::redefineLocalDomain + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::selectDB + * @covers \Wikimedia\Rdbms\DatabaseMssql::selectDB + * @covers DatabaseOracle::selectDB + */ public function testRedefineLocalDomain() { global $wgDBname; @@ -642,10 +706,10 @@ class LBFactoryTest extends MediaWikiTestCase { ); unset( $conn1 ); - $factory->redefineLocalDomain( 'somedb-prefix' ); - $this->assertEquals( 'somedb-prefix', $factory->getLocalDomainID() ); + $factory->redefineLocalDomain( 'somedb-prefix_' ); + $this->assertEquals( 'somedb-prefix_', $factory->getLocalDomainID() ); - $domain = new DatabaseDomain( $wgDBname, null, 'pref' ); + $domain = new DatabaseDomain( $wgDBname, null, 'pref_' ); $factory->redefineLocalDomain( $domain ); $n = 0; @@ -665,7 +729,7 @@ class LBFactoryTest extends MediaWikiTestCase { $factory->destroy(); } - private function quoteTable( Database $db, $table ) { + private function quoteTable( IDatabase $db, $table ) { if ( $db->getType() === 'sqlite' ) { return $table; } else { diff --git a/tests/phpunit/includes/db/LoadBalancerTest.php b/tests/phpunit/includes/db/LoadBalancerTest.php index 2c4e6b421b..4291bccd80 100644 --- a/tests/phpunit/includes/db/LoadBalancerTest.php +++ b/tests/phpunit/includes/db/LoadBalancerTest.php @@ -32,7 +32,7 @@ use Wikimedia\Rdbms\LoadMonitorNull; * @covers \Wikimedia\Rdbms\LoadBalancer */ class LoadBalancerTest extends MediaWikiTestCase { - private function makeServerConfig() { + private function makeServerConfig( $flags = DBO_DEFAULT ) { global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; return [ @@ -44,7 +44,7 @@ class LoadBalancerTest extends MediaWikiTestCase { 'type' => $wgDBtype, 'dbDirectory' => $wgSQLiteDataDir, 'load' => 0, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency + 'flags' => $flags ]; } @@ -57,7 +57,8 @@ class LoadBalancerTest extends MediaWikiTestCase { $called = false; $lb = new LoadBalancer( [ - 'servers' => [ $this->makeServerConfig() ], + // Simulate web request with DBO_TRX + 'servers' => [ $this->makeServerConfig( DBO_TRX ) ], 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), 'chronologyCallback' => function () use ( &$called ) { @@ -71,8 +72,8 @@ class LoadBalancerTest extends MediaWikiTestCase { $this->assertSame( 'my_test_wiki', $lb->resolveDomainID( 'my_test_wiki' ) ); $this->assertSame( $ld->getId(), $lb->resolveDomainID( false ) ); $this->assertSame( $ld->getId(), $lb->resolveDomainID( $ld ) ); - $this->assertFalse( $called ); + $dbw = $lb->getConnection( DB_MASTER ); $this->assertTrue( $called ); $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' ); @@ -106,39 +107,10 @@ class LoadBalancerTest extends MediaWikiTestCase { } public function testWithReplica() { - global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; - - $servers = [ - [ // master - 'host' => $wgDBserver, - 'dbname' => $wgDBname, - 'tablePrefix' => $this->dbPrefix(), - 'user' => $wgDBuser, - 'password' => $wgDBpassword, - 'type' => $wgDBtype, - 'dbDirectory' => $wgSQLiteDataDir, - 'load' => 0, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency - ], - [ // emulated replica - 'host' => $wgDBserver, - 'dbname' => $wgDBname, - 'tablePrefix' => $this->dbPrefix(), - 'user' => $wgDBuser, - 'password' => $wgDBpassword, - 'type' => $wgDBtype, - 'dbDirectory' => $wgSQLiteDataDir, - 'load' => 100, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency - ] - ]; + global $wgDBserver; - $lb = new LoadBalancer( [ - 'servers' => $servers, - 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), - 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), - 'loadMonitorClass' => LoadMonitorNull::class - ] ); + // Simulate web request with DBO_TRX + $lb = $this->newMultiServerLocalLoadBalancer( DBO_TRX ); $dbw = $lb->getConnection( DB_MASTER ); $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' ); @@ -180,6 +152,51 @@ class LoadBalancerTest extends MediaWikiTestCase { $lb->closeAll(); } + private function newSingleServerLocalLoadBalancer() { + global $wgDBname; + + return new LoadBalancer( [ + 'servers' => [ $this->makeServerConfig() ], + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ) + ] ); + } + + private function newMultiServerLocalLoadBalancer( $flags = DBO_DEFAULT ) { + global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; + + $servers = [ + [ // master + 'host' => $wgDBserver, + 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), + 'user' => $wgDBuser, + 'password' => $wgDBpassword, + 'type' => $wgDBtype, + 'dbDirectory' => $wgSQLiteDataDir, + 'load' => 0, + 'flags' => $flags + ], + [ // emulated replica + 'host' => $wgDBserver, + 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), + 'user' => $wgDBuser, + 'password' => $wgDBpassword, + 'type' => $wgDBtype, + 'dbDirectory' => $wgSQLiteDataDir, + 'load' => 100, + 'flags' => $flags + ] + ]; + + return new LoadBalancer( [ + 'servers' => $servers, + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), + 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), + 'loadMonitorClass' => LoadMonitorNull::class + ] ); + } + private function assertWriteForbidden( Database $db ) { try { $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ ); @@ -286,20 +303,20 @@ class LoadBalancerTest extends MediaWikiTestCase { * @covers LoadBalancer::getAnyOpenConnection() */ function testOpenConnection() { - global $wgDBname; - - $lb = new LoadBalancer( [ - 'servers' => [ $this->makeServerConfig() ], - 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ) - ] ); + $lb = $this->newSingleServerLocalLoadBalancer(); $i = $lb->getWriterIndex(); $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) ); + $conn1 = $lb->getConnection( $i ); $this->assertNotEquals( null, $conn1 ); $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) ); + $this->assertFalse( $conn1->getFlag( DBO_TRX ) ); + $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ); $this->assertNotEquals( null, $conn2 ); + $this->assertFalse( $conn2->getFlag( DBO_TRX ) ); + if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) { $this->assertEquals( null, $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) ); @@ -343,7 +360,7 @@ class LoadBalancerTest extends MediaWikiTestCase { 'type' => $wgDBtype, 'dbDirectory' => $wgSQLiteDataDir, 'load' => 0, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency + 'flags' => DBO_TRX // simulate a web request with DBO_TRX ], ]; @@ -416,4 +433,60 @@ class LoadBalancerTest extends MediaWikiTestCase { $conn1->close(); $conn2->close(); } + + public function testDBConnRefReadsMasterAndReplicaRoles() { + $lb = $this->newSingleServerLocalLoadBalancer(); + + $rConn = $lb->getConnectionRef( DB_REPLICA ); + $wConn = $lb->getConnectionRef( DB_MASTER ); + $wConn2 = $lb->getConnectionRef( 0 ); + + $v = [ 'value' => '1', '1' ]; + $sql = 'SELECT MAX(1) AS value'; + foreach ( [ $rConn, $wConn, $wConn2 ] as $conn ) { + $conn->clearFlag( $conn::DBO_TRX ); + + $res = $conn->query( $sql, __METHOD__ ); + $this->assertEquals( $v, $conn->fetchRow( $res ) ); + + $res = $conn->query( $sql, __METHOD__, $conn::QUERY_REPLICA_ROLE ); + $this->assertEquals( $v, $conn->fetchRow( $res ) ); + } + + $wConn->getScopedLockAndFlush( 'key', __METHOD__, 1 ); + $wConn2->getScopedLockAndFlush( 'key2', __METHOD__, 1 ); + } + + /** + * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError + */ + public function testDBConnRefWritesReplicaRole() { + $lb = $this->newSingleServerLocalLoadBalancer(); + + $rConn = $lb->getConnectionRef( DB_REPLICA ); + + $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' ); + } + + /** + * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError + */ + public function testDBConnRefWritesReplicaRoleIndex() { + $lb = $this->newMultiServerLocalLoadBalancer(); + + $rConn = $lb->getConnectionRef( 1 ); + + $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' ); + } + + /** + * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError + */ + public function testDBConnRefWritesReplicaRoleInsert() { + $lb = $this->newMultiServerLocalLoadBalancer(); + + $rConn = $lb->getConnectionRef( DB_REPLICA ); + + $rConn->insert( 'test', [ 't' => 1 ], __METHOD__ ); + } } diff --git a/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php index 7d0c83912c..b30c7a4c92 100644 --- a/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php +++ b/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php @@ -3,6 +3,9 @@ namespace MediaWiki\Logger\Monolog; /** + * Flay per https://phabricator.wikimedia.org/T218688. + * + * @group Broken * @covers \MediaWiki\Logger\Monolog\CeeFormatter */ class CeeFormatterTest extends \PHPUnit\Framework\TestCase { diff --git a/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php index 1ee188e7cd..a1207b26b2 100644 --- a/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php +++ b/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php @@ -5,6 +5,7 @@ namespace MediaWiki\Logger\Monolog; class LogstashFormatterTest extends \PHPUnit\Framework\TestCase { /** * @dataProvider provideV1 + * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1 * @param array $record The input record. * @param array $expected Associative array of expected keys and their values. * @param array $notExpected List of keys that should not exist. @@ -42,6 +43,9 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase { ]; } + /** + * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1 + */ public function testV1WithPrefix() { $formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 ); $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ]; diff --git a/tests/phpunit/includes/deferred/LinksUpdateTest.php b/tests/phpunit/includes/deferred/LinksUpdateTest.php index ddc0798f1a..cd3ddfa8e8 100644 --- a/tests/phpunit/includes/deferred/LinksUpdateTest.php +++ b/tests/phpunit/includes/deferred/LinksUpdateTest.php @@ -118,6 +118,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase { /** * @covers ParserOutput::addExternalLink + * @covers LinksUpdate::getAddedExternalLinks + * @covers LinksUpdate::getRemovedExternalLinks */ public function testUpdate_externallinks() { /** @var ParserOutput $po */ @@ -125,7 +127,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $po->addExternalLink( "http://testing.com/wiki/Foo" ); - $this->assertLinksUpdate( + $update = $this->assertLinksUpdate( $t, $po, 'externallinks', @@ -135,6 +137,31 @@ class LinksUpdateTest extends MediaWikiLangTestCase { [ 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ], ] ); + + $this->assertArrayEquals( [ + "http://testing.com/wiki/Foo" + ], $update->getAddedExternalLinks() ); + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + $po->addExternalLink( 'http://testing.com/wiki/Bar' ); + $update = $this->assertLinksUpdate( + $t, + $po, + 'externallinks', + 'el_to, el_index', + 'el_from = ' . self::$testingPageId, + [ + [ 'http://testing.com/wiki/Bar', 'http://com.testing./wiki/Bar' ], + ] + ); + + $this->assertArrayEquals( [ + "http://testing.com/wiki/Bar" + ], $update->getAddedExternalLinks() ); + $this->assertArrayEquals( [ + "http://testing.com/wiki/Foo" + ], $update->getRemovedExternalLinks() ); } /** @@ -379,33 +406,17 @@ class LinksUpdateTest extends MediaWikiLangTestCase { protected function assertRecentChangeByCategorization( Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows ) { - global $wgCommentTableSchemaMigrationStage; - - if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) { - $this->assertSelect( - 'recentchanges', - 'rc_title, rc_comment', - [ - 'rc_type' => RC_CATEGORIZE, - 'rc_namespace' => NS_CATEGORY, - 'rc_title' => $categoryTitle->getDBkey() - ], - $expectedRows - ); - } - if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) { - $this->assertSelect( - [ 'recentchanges', 'comment' ], - 'rc_title, comment_text', - [ - 'rc_type' => RC_CATEGORIZE, - 'rc_namespace' => NS_CATEGORY, - 'rc_title' => $categoryTitle->getDBkey(), - 'comment_id = rc_comment_id', - ], - $expectedRows - ); - } + $this->assertSelect( + [ 'recentchanges', 'comment' ], + 'rc_title, comment_text', + [ + 'rc_type' => RC_CATEGORIZE, + 'rc_namespace' => NS_CATEGORY, + 'rc_title' => $categoryTitle->getDBkey(), + 'comment_id = rc_comment_id', + ], + $expectedRows + ); } private function runAllRelatedJobs() { diff --git a/tests/phpunit/includes/deferred/SearchUpdateTest.php b/tests/phpunit/includes/deferred/SearchUpdateTest.php index 9e4dbea221..74a5e3c470 100644 --- a/tests/phpunit/includes/deferred/SearchUpdateTest.php +++ b/tests/phpunit/includes/deferred/SearchUpdateTest.php @@ -1,20 +1,5 @@ getNativeData() . '|' . $new->getNativeData(); + return $old->getText() . '|' . $new->getText(); } public function showDiffStyle() { diff --git a/tests/phpunit/includes/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php index f570f55329..4dc2f9e0ca 100644 --- a/tests/phpunit/includes/filebackend/FileBackendTest.php +++ b/tests/phpunit/includes/filebackend/FileBackendTest.php @@ -350,7 +350,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ), "Destination file $dest does not exist ($backendName)." ); - return; // done + return; } $status = $this->backend->doOperation( @@ -470,7 +470,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ), "Destination file $dest does not exist ($backendName)." ); - return; // done + return; } $status = $this->backend->doOperation( diff --git a/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php index 99bea68dbf..eaba22d7fb 100644 --- a/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php +++ b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php @@ -1,7 +1,5 @@ [ 'r1', 'r2' ], 'columns' => [ 'c1', 'c2' ], 'fieldname' => 'test', diff --git a/tests/phpunit/includes/import/ImportTest.php b/tests/phpunit/includes/import/ImportTest.php index 3b91f5b3d5..2b8122287f 100644 --- a/tests/phpunit/includes/import/ImportTest.php +++ b/tests/phpunit/includes/import/ImportTest.php @@ -33,7 +33,7 @@ class ImportTest extends MediaWikiLangTestCase { $title = Title::newFromText( $title ); $this->assertTrue( $title->exists() ); - $this->assertEquals( WikiPage::factory( $title )->getContent()->getNativeData(), $text ); + $this->assertEquals( WikiPage::factory( $title )->getContent()->getText(), $text ); } public function getUnknownTagsXML() { @@ -222,6 +222,9 @@ EOF /** * @dataProvider provideUnknownUserHandling + * @covers WikiImporter::setUsernamePrefix + * @covers ExternalUserNames::addPrefix + * @covers ExternalUserNames::applyPrefix * @param bool $assign * @param bool $create */ diff --git a/tests/phpunit/includes/installer/OracleInstallerTest.php b/tests/phpunit/includes/installer/OracleInstallerTest.php index 2811a9cf41..e255089de4 100644 --- a/tests/phpunit/includes/installer/OracleInstallerTest.php +++ b/tests/phpunit/includes/installer/OracleInstallerTest.php @@ -1,8 +1,6 @@ JobQueueMemory::class, - 'wiki' => wfWikiID(), + 'domain' => WikiMap::getCurrentWikiDbDomain()->getId(), 'type' => 'null', ] ); } diff --git a/tests/phpunit/includes/jobqueue/JobQueueTest.php b/tests/phpunit/includes/jobqueue/JobQueueTest.php index 0421fe7c68..81a80b66d5 100644 --- a/tests/phpunit/includes/jobqueue/JobQueueTest.php +++ b/tests/phpunit/includes/jobqueue/JobQueueTest.php @@ -31,7 +31,9 @@ class JobQueueTest extends MediaWikiTestCase { $baseConfig = [ 'class' => JobQueueDBSingle::class ]; } $baseConfig['type'] = 'null'; - $baseConfig['wiki'] = wfWikiID(); + $baseConfig['domain'] = WikiMap::getCurrentWikiDbDomain()->getId(); + $baseConfig['stash'] = new HashBagOStuff(); + $baseConfig['wanCache'] = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); $variants = [ 'queueRand' => [ 'order' => 'random', 'claimTTL' => 0 ], 'queueRandTTL' => [ 'order' => 'random', 'claimTTL' => 10 ], @@ -75,7 +77,10 @@ class JobQueueTest extends MediaWikiTestCase { $this->markTestSkipped( $desc ); } $this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" ); - $this->assertEquals( wfWikiID(), $queue->getDomain(), "Proper wiki ID ($desc)" ); + $this->assertEquals( + WikiMap::getCurrentWikiDbDomain()->getId(), + $queue->getDomain(), + "Proper wiki ID ($desc)" ); } /** diff --git a/tests/phpunit/includes/jobqueue/JobTest.php b/tests/phpunit/includes/jobqueue/JobTest.php index 0cab702439..769b1930fe 100644 --- a/tests/phpunit/includes/jobqueue/JobTest.php +++ b/tests/phpunit/includes/jobqueue/JobTest.php @@ -78,7 +78,7 @@ class JobTest extends MediaWikiTestCase { '{"file":"db1023-bin.001288","pos":"308257743","asOfTime":' . // Embed dynamically because TestSetup sets serialize_precision=17 // which, in PHP 7.1 and 7.2, produces 1457521464.3814001 instead - json_encode( 1457521464.3814 ) . '} ' . 'triggeredRecursive=1 ' . + json_encode( 1457521464.3814 ) . '} triggeredRecursive=1 ' . $requestId ], ]; diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php index 2760cb9f22..a6adf343d5 100644 --- a/tests/phpunit/includes/json/FormatJsonTest.php +++ b/tests/phpunit/includes/json/FormatJsonTest.php @@ -109,6 +109,15 @@ class FormatJsonTest extends MediaWikiTestCase { ); } + public function testEncodeFail() { + // Set up a recursive object that can't be encoded. + $a = new stdClass; + $b = new stdClass; + $a->b = $b; + $b->a = $a; + $this->assertFalse( FormatJson::encode( $a ) ); + } + public function testDecodeReturnType() { $this->assertInternalType( 'object', diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php index 9ec660e2c5..e04b2e21bf 100644 --- a/tests/phpunit/includes/libs/IEUrlExtensionTest.php +++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php @@ -1,8 +1,5 @@ cache ) ) { - $success = true; - return $this->cache[$key]; - } - $success = false; - return false; - } - - protected function storeResult( $key, $result ) { - $this->cache[$key] = $result; - } -} - -/** - * PHP Unit tests for MemoizedCallable class. + * PHPUnit tests for MemoizedCallable class. * @covers MemoizedCallable */ class MemoizedCallableTest extends PHPUnit\Framework\TestCase { @@ -140,3 +119,24 @@ class MemoizedCallableTest extends PHPUnit\Framework\TestCase { $memoized = new MemoizedCallable( 14 ); } } + +/** + * A MemoizedCallable subclass that stores function return values + * in an instance property rather than APC or APCu. + */ +class ArrayBackedMemoizedCallable extends MemoizedCallable { + private $cache = []; + + protected function fetchResult( $key, &$success ) { + if ( array_key_exists( $key, $this->cache ) ) { + $success = true; + return $this->cache[$key]; + } + $success = false; + return false; + } + + protected function storeResult( $key, $result ) { + $this->cache[$key] = $result; + } +} diff --git a/tests/phpunit/includes/libs/ProcessCacheLRUTest.php b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php index c9fa3205fe..8e91e70cb0 100644 --- a/tests/phpunit/includes/libs/ProcessCacheLRUTest.php +++ b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php @@ -1,8 +1,6 @@ getCliArg( 'use-bagostuff' ) ) { + if ( $this->getCliArg( 'use-bagostuff' ) !== null ) { $name = $this->getCliArg( 'use-bagostuff' ); $this->cache = ObjectCache::newFromId( $name ); @@ -26,6 +26,7 @@ class BagOStuffTest extends MediaWikiTestCase { } $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) ); + $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) . ':lock' ); } /** @@ -64,14 +65,19 @@ class BagOStuffTest extends MediaWikiTestCase { /** * @covers BagOStuff::merge - * @covers BagOStuff::mergeViaLock * @covers BagOStuff::mergeViaCas */ public function testMerge() { - $calls = 0; $key = $this->cache->makeKey( self::TEST_KEY ); - $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls ) { + + $calls = 0; + $casRace = false; // emulate a race + $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls, &$casRace ) { ++$calls; + if ( $casRace ) { + // Uses CAS instead? + $cache->set( $key, 'conflict', 5 ); + } return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged'; }; @@ -87,67 +93,18 @@ class BagOStuffTest extends MediaWikiTestCase { $this->assertEquals( 'mergedmerged', $this->cache->get( $key ) ); $calls = 0; - $this->cache->lock( $key ); - $this->assertFalse( $this->cache->merge( $key, $callback, 1 ), 'Non-blocking merge' ); - $this->cache->unlock( $key ); - $this->assertEquals( 0, $calls ); - } - - /** - * @covers BagOStuff::merge - * @covers BagOStuff::mergeViaLock - */ - public function testMerge_fork() { - $key = $this->cache->makeKey( self::TEST_KEY ); - $callback = function ( BagOStuff $cache, $key, $oldVal ) { - return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged'; - }; - /* - * Test concurrent merges by forking this process, if: - * - not manually called with --use-bagostuff - * - pcntl_fork is supported by the system - * - cache type will correctly support calls over forks - */ - $fork = (bool)$this->getCliArg( 'use-bagostuff' ); - $fork &= function_exists( 'pcntl_fork' ); - $fork &= !$this->cache instanceof HashBagOStuff; - $fork &= !$this->cache instanceof EmptyBagOStuff; - $fork &= !$this->cache instanceof MultiWriteBagOStuff; - if ( $fork ) { - $pid = null; - // Function to start merge(), run another merge() midway through, then finish - $outerFunc = function ( BagOStuff $cache, $key, $oldVal ) use ( $callback, &$pid ) { - $pid = pcntl_fork(); - if ( $pid == -1 ) { - return false; - } elseif ( $pid ) { - pcntl_wait( $status ); - - return $callback( $cache, $key, $oldVal ); - } else { - $this->cache->merge( $key, $callback, 0, 1 ); - // Bail out of the outer merge() in the child process since it does not - // need to attempt to write anything. Success is checked by the parent. - parent::tearDown(); // avoid phpunit notices - exit; - } - }; - - // attempt a merge - this should fail - $merged = $this->cache->merge( $key, $outerFunc, 0, 1 ); - - if ( $pid == -1 ) { - return; // can't fork, ignore this test... - } - - // merge has failed because child process was merging (and we only attempted once) - $this->assertFalse( $merged ); - - // make sure the child's merge is completed and verify - $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' ); + $casRace = true; + $this->assertFalse( + $this->cache->merge( $key, $callback, 5, 1 ), + 'Non-blocking merge (CAS)' + ); + if ( $this->cache instanceof MultiWriteBagOStuff ) { + $wrapper = \Wikimedia\TestingAccessWrapper::newFromObject( $this->cache ); + $n = count( $wrapper->caches ); } else { - $this->markTestSkipped( 'No pcntl methods available' ); + $n = 1; } + $this->assertEquals( $n, $calls ); } /** @@ -266,6 +223,34 @@ class BagOStuffTest extends MediaWikiTestCase { $this->cache->delete( $key4 ); } + /** + * @covers BagOStuff::setMulti + * @covers BagOStuff::deleteMulti + */ + public function testSetDeleteMulti() { + $map = [ + $this->cache->makeKey( 'test-1' ) => 'Siberian', + $this->cache->makeKey( 'test-2' ) => [ 'Huskies' ], + $this->cache->makeKey( 'test-3' ) => [ 'are' => 'the' ], + $this->cache->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ], + $this->cache->makeKey( 'test-5' ) => 4, + $this->cache->makeKey( 'test-6' ) => 'ever' + ]; + + $this->cache->setMulti( $map, 5 ); + $this->assertEquals( + $map, + $this->cache->getMulti( array_keys( $map ) ) + ); + + $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ), 5 ) ); + + $this->assertEquals( + [], + $this->cache->getMulti( array_keys( $map ) ) + ); + } + /** * @covers BagOStuff::getScopedLock */ diff --git a/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php index d0360a99f2..f953319e67 100644 --- a/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php @@ -11,7 +11,7 @@ class CachedBagOStuffTest extends PHPUnit\Framework\TestCase { /** * @covers CachedBagOStuff::__construct - * @covers CachedBagOStuff::doGet + * @covers CachedBagOStuff::get */ public function testGetFromBackend() { $backend = new HashBagOStuff; @@ -36,6 +36,7 @@ class CachedBagOStuffTest extends PHPUnit\Framework\TestCase { $cache->set( "key$i", 1 ); $this->assertEquals( 1, $cache->get( "key$i" ) ); $this->assertEquals( 1, $backend->get( "key$i" ) ); + $cache->delete( "key$i" ); $this->assertEquals( false, $cache->get( "key$i" ) ); $this->assertEquals( false, $backend->get( "key$i" ) ); @@ -67,7 +68,7 @@ class CachedBagOStuffTest extends PHPUnit\Framework\TestCase { } /** - * @covers CachedBagOStuff::doGet + * @covers CachedBagOStuff::get */ public function testCacheBackendMisses() { $backend = new HashBagOStuff; diff --git a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php index 8a95ae7a16..0376803f41 100644 --- a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php @@ -138,6 +138,9 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { $this->assertSame( 'special', $cache->makeGlobalKey( 'a', 'b' ) ); } + /** + * @covers MultiWriteBagOStuff::add + */ public function testDuplicateStoreAdd() { $bag = new HashBagOStuff(); $cache = new MultiWriteBagOStuff( [ diff --git a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php index 3e5211548e..017d745e49 100644 --- a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php +++ b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php @@ -30,9 +30,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { parent::setUp(); $this->cache = new WANObjectCache( [ - 'cache' => new HashBagOStuff(), - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ) + 'cache' => new HashBagOStuff() ] ); $wanCache = TestingAccessWrapper::newFromObject( $this->cache ); @@ -123,6 +121,9 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { } public function testProcessCache() { + $mockWallClock = 1549343530.2053; + $this->cache->setMockTime( $mockWallClock ); + $hit = 0; $callback = function () use ( &$hit ) { ++$hit; @@ -156,18 +157,28 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( 6, $hit, "New values cached" ); foreach ( $keys as $i => $key ) { + // Should evict from process cache $this->cache->delete( $key ); + $mockWallClock += 0.001; // cached values will be newer than tombstone + // Get into cache (specific process cache group) $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] ); } - $this->assertEquals( 9, $hit, "Values evicted" ); + $this->assertEquals( 9, $hit, "Values evicted by delete()" ); - $key = reset( $keys ); // Get into cache (default process cache group) + $key = reset( $keys ); + $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] ); + $this->assertEquals( 9, $hit, "Value recently interim-cached" ); + + $mockWallClock += 0.2; // interim key not brand new + $this->cache->clearProcessCache(); $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] ); - $this->assertEquals( 10, $hit, "Value calculated" ); + $this->assertEquals( 10, $hit, "Value calculated (interim key not recent and reset)" ); $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] ); - $this->assertEquals( 10, $hit, "Value cached" ); + $this->assertEquals( 10, $hit, "Value process cached" ); + + $mockWallClock += 0.2; // interim key not brand new $outerCallback = function () use ( &$callback, $key ) { $v = $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] ); @@ -205,6 +216,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { return $value; }; + $mockWallClock = 1549343530.2053; + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + $wasSet = 0; $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts ); $this->assertEquals( $value, $v, "Value returned" ); @@ -223,10 +238,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( $value, $v, "Value returned" ); $this->assertEquals( 0, $wasSet, "Value not regenerated" ); - $mockWallClock = microtime( true ); - $priorTime = $mockWallClock; // reference time - $cache->setMockTime( $mockWallClock ); - $mockWallClock += 1; $wasSet = 0; @@ -242,7 +253,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $t2 = $cache->getCheckKeyTime( $cKey2 ); $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' ); - $mockWallClock += 0.01; + $mockWallClock += 0.2; // interim key is not brand new and check keys have past values $priorTime = $mockWallClock; // reference time $wasSet = 0; $v = $cache->getWithSetCallback( @@ -284,7 +295,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { return 'xxx' . $wasSet; }; - $mockWallClock = microtime( true ); + $mockWallClock = 1549343530.2053; $priorTime = $mockWallClock; // reference time $wasSet = 0; @@ -374,7 +385,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { function testGetWithSetcallback_touched( array $extOpts, $versioned ) { $cache = $this->cache; - $mockWallClock = microtime( true ); + $mockWallClock = 1549343530.2053; $cache->setMockTime( $mockWallClock ); $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf ) @@ -439,10 +450,9 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { return $value; }; - $cache = new NearExpiringWANObjectCache( [ - 'cache' => new HashBagOStuff(), - 'pool' => 'empty', - ] ); + $cache = new NearExpiringWANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $mockWallClock = 1549343530.2053; + $cache->setMockTime( $mockWallClock ); $wasSet = 0; $key = wfRandomString(); @@ -450,6 +460,8 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $v = $cache->getWithSetCallback( $key, 20, $func, $opts ); $this->assertEquals( $value, $v, "Value returned" ); $this->assertEquals( 1, $wasSet, "Value calculated" ); + + $mockWallClock += 0.2; // interim key is not brand new $v = $cache->getWithSetCallback( $key, 20, $func, $opts ); $this->assertEquals( 2, $wasSet, "Value re-calculated" ); @@ -468,11 +480,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { }; $cache = new NearExpiringWANObjectCache( [ 'cache' => new HashBagOStuff(), - 'pool' => 'empty', 'asyncHandler' => $asyncHandler ] ); - $mockWallClock = microtime( true ); + $mockWallClock = 1549343530.2053; $priorTime = $mockWallClock; // reference time $cache->setMockTime( $mockWallClock ); @@ -500,8 +511,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( $value, $v, "New value stored" ); $cache = new PopularityRefreshingWANObjectCache( [ - 'cache' => new HashBagOStuff(), - 'pool' => 'empty' + 'cache' => new HashBagOStuff() ] ); $mockWallClock = $priorTime; @@ -573,6 +583,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { return "@$id$"; }; + $mockWallClock = 1549343530.2053; + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + $wasSet = 0; $keyedIds = new ArrayIterator( [ $keyA => 3353 ] ); $value = "@3353$"; @@ -602,10 +616,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( 1, $wasSet, "Value not regenerated" ); $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" ); - $mockWallClock = microtime( true ); - $priorTime = $mockWallClock; // reference time - $cache->setMockTime( $mockWallClock ); - $mockWallClock += 1; $wasSet = 0; @@ -694,7 +704,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { WANObjectCache::VALUE_KEY_PREFIX . 'k1' => 'val-id1', WANObjectCache::VALUE_KEY_PREFIX . 'k2' => 'val-id2' ] ); - $wanCache = new WANObjectCache( [ 'cache' => $localBag, 'pool' => 'testcache-hash' ] ); + $wanCache = new WANObjectCache( [ 'cache' => $localBag ] ); // Warm the process cache $keyedIds = new ArrayIterator( [ 'k1' => 'id1', 'k2' => 'id2' ] ); @@ -746,6 +756,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { return $newValues; }; + $mockWallClock = 1549343530.2053; + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + $wasSet = 0; $keyedIds = new ArrayIterator( [ $keyA => 3353 ] ); $value = "@3353$"; @@ -773,10 +787,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( 1, $wasSet, "Value not regenerated" ); $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" ); - $mockWallClock = microtime( true ); - $priorTime = $mockWallClock; // reference time - $cache->setMockTime( $mockWallClock ); - $mockWallClock += 1; $wasSet = 0; @@ -877,6 +887,9 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $key = wfRandomString(); $value = wfRandomString(); + $mockWallClock = 1549343530.2053; + $cache->setMockTime( $mockWallClock ); + $calls = 0; $func = function () use ( &$calls, $value, $cache, $key ) { ++$calls; @@ -897,6 +910,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( 1, $calls, 'Callback was not used' ); $cache->delete( $key ); + $mockWallClock += 0.001; // cached values will be newer than tombstone $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] ); $this->assertEquals( $value, $ret, 'Callback was used; interim saved' ); @@ -916,30 +930,77 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { public function testLockTSESlow() { $cache = $this->cache; $key = wfRandomString(); + $key2 = wfRandomString(); $value = wfRandomString(); + $mockWallClock = 1549343530.2053; + $cache->setMockTime( $mockWallClock ); + $calls = 0; - $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) { + $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, &$mockWallClock ) { ++$calls; - $setOpts['since'] = microtime( true ) - 10; - // Immediately kill any mutex rather than waiting a second - $cache->delete( $cache::MUTEX_KEY_PREFIX . $key ); + $setOpts['since'] = $mockWallClock - 10; return $value; }; - // Value should be marked as stale due to snapshot lag + // Value should be given a low logical TTL due to snapshot lag $curTTL = null; - $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] ); + $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] ); $this->assertEquals( $value, $ret ); $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' ); - $this->assertLessThan( 0, $curTTL, 'Value has negative curTTL' ); + $this->assertEquals( 1, $curTTL, 'Value has reduced logical TTL', 0.01 ); $this->assertEquals( 1, $calls, 'Value was generated' ); + $mockWallClock += 2; // low logical TTL expired + + $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 2, $calls, 'Callback used (mutex acquired)' ); + + $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 2, $calls, 'Callback was not used (interim value used)' ); + + $mockWallClock += 2; // low logical TTL expired // Acquire a lock to verify that getWithSetCallback uses lockTSE properly $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); - $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] ); + + $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] ); $this->assertEquals( $value, $ret ); - $this->assertEquals( 1, $calls, 'Callback was not used' ); + $this->assertEquals( 2, $calls, 'Callback was not used (mutex not acquired)' ); + + $mockWallClock += 301; // physical TTL expired + // Acquire a lock to verify that getWithSetCallback uses lockTSE properly + $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); + + $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 3, $calls, 'Callback was used (mutex not acquired, not in cache)' ); + + $calls = 0; + $func2 = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value ) { + ++$calls; + $setOpts['lag'] = 15; + return $value; + }; + + // Value should be given a low logical TTL due to replication lag + $curTTL = null; + $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( $value, $cache->get( $key2, $curTTL ), 'Value was populated' ); + $this->assertEquals( 30, $curTTL, 'Value has reduced logical TTL', 0.01 ); + $this->assertEquals( 1, $calls, 'Value was generated' ); + + $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 1, $calls, 'Callback was used (not expired)' ); + + $mockWallClock += 31; + + $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] ); + $this->assertEquals( $value, $ret ); + $this->assertEquals( 2, $calls, 'Callback was used (mutex acquired)' ); } /** @@ -952,11 +1013,12 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $value = wfRandomString(); $busyValue = wfRandomString(); + $mockWallClock = 1549343530.2053; + $cache->setMockTime( $mockWallClock ); + $calls = 0; $func = function () use ( &$calls, $value, $cache, $key ) { ++$calls; - // Immediately kill any mutex rather than waiting a second - $cache->delete( $cache::MUTEX_KEY_PREFIX . $key ); return $value; }; @@ -964,6 +1026,8 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( $value, $ret ); $this->assertEquals( 1, $calls, 'Value was populated' ); + $mockWallClock += 0.2; // interim keys not brand new + // Acquire a lock to verify that getWithSetCallback uses busyValue properly $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); @@ -985,6 +1049,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' ); $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key ); + $mockWallClock += 0.001; // cached values will be newer than tombstone $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] ); $this->assertEquals( $value, $ret, 'Callback was used; saved interim' ); @@ -1010,6 +1075,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $key2 = wfRandomString(); $key3 = wfRandomString(); + $mockWallClock = 1549343530.2053; + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + $cache->set( $key1, $value1, 5 ); $cache->set( $key2, $value2, 10 ); @@ -1027,10 +1096,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $cKey1 = wfRandomString(); $cKey2 = wfRandomString(); - $mockWallClock = microtime( true ); - $priorTime = $mockWallClock; // reference time - $cache->setMockTime( $mockWallClock ); - $mockWallClock += 1; $curTTLs = []; @@ -1074,7 +1139,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $value1 = wfRandomString(); $value2 = wfRandomString(); - $mockWallClock = microtime( true ); + $mockWallClock = 1549343530.2053; $cache->setMockTime( $mockWallClock ); // Fake initial check key to be set in the past. Otherwise we'd have to sleep for @@ -1305,6 +1370,9 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { public function testInterimHoldOffCaching() { $cache = $this->cache; + $mockWallClock = 1549343530.2053; + $cache->setMockTime( $mockWallClock ); + $value = 'CRL-40-940'; $wasCalled = 0; $func = function () use ( &$wasCalled, $value ) { @@ -1319,10 +1387,16 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $v = $cache->getWithSetCallback( $key, 60, $func ); $v = $cache->getWithSetCallback( $key, 60, $func ); $this->assertEquals( 1, $wasCalled, 'Value cached' ); + $cache->delete( $key ); + $mockWallClock += 0.001; // cached values will be newer than tombstone $v = $cache->getWithSetCallback( $key, 60, $func ); $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 2, $wasCalled, 'Value interim cached' ); // reuses interim + + $mockWallClock += 0.2; // interim key not brand new + $v = $cache->getWithSetCallback( $key, 60, $func ); $this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim // Lock up the mutex so interim cache is used $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); @@ -1362,7 +1436,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $cache = $this->cache; $key = wfRandomString(); - $mockWallClock = microtime( true ); + $mockWallClock = 1549343530.2053; $priorTime = $mockWallClock; // reference time $cache->setMockTime( $mockWallClock ); @@ -1405,18 +1479,22 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $tKey2 = wfRandomString(); $value = 'meow'; + $mockWallClock = 1549343530.2053; + $priorTime = $mockWallClock; // reference time + $this->cache->setMockTime( $mockWallClock ); + // Two check keys are newer (given hold-off) than $key, another is older $this->internalCache->set( WANObjectCache::TIME_KEY_PREFIX . $tKey2, - WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 3 ) + WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 3 ) ); $this->internalCache->set( WANObjectCache::TIME_KEY_PREFIX . $tKey2, - WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 5 ) + WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 5 ) ); $this->internalCache->set( WANObjectCache::TIME_KEY_PREFIX . $tKey1, - WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 30 ) + WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 30 ) ); $this->cache->set( $key, $value, 30 ); @@ -1500,9 +1578,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { ->willReturn( false ); $wanCache = new WANObjectCache( [ - 'cache' => $backend, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ) + 'cache' => $backend ] ); $isStale = null; @@ -1552,8 +1628,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $localBag->expects( $this->never() )->method( 'delete' ); $wanCache = new WANObjectCache( [ 'cache' => $localBag, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ), 'mcrouterAware' => true, 'region' => 'pmtpa', 'cluster' => 'mw-wan' @@ -1578,8 +1652,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { ->setMethods( [ 'set' ] )->getMock(); $wanCache = new WANObjectCache( [ 'cache' => $localBag, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ), 'mcrouterAware' => true, 'region' => 'pmtpa', 'cluster' => 'mw-wan' @@ -1596,8 +1668,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { ->setMethods( [ 'set' ] )->getMock(); $wanCache = new WANObjectCache( [ 'cache' => $localBag, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ), 'mcrouterAware' => true, 'region' => 'pmtpa', 'cluster' => 'mw-wan' @@ -1614,8 +1684,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { ->setMethods( [ 'delete' ] )->getMock(); $wanCache = new WANObjectCache( [ 'cache' => $localBag, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ), 'mcrouterAware' => true, 'region' => 'pmtpa', 'cluster' => 'mw-wan' @@ -1629,7 +1697,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { public function testEpoch() { $bag = new HashBagOStuff(); - $cache = new WANObjectCache( [ 'cache' => $bag, 'pool' => 'testcache-hash' ] ); + $cache = new WANObjectCache( [ 'cache' => $bag ] ); $key = $cache->makeGlobalKey( 'The whole of the Law' ); $now = microtime( true ); @@ -1645,7 +1713,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $cache = new WANObjectCache( [ 'cache' => $bag, - 'pool' => 'testcache-hash', 'epoch' => $now - 3600 ] ); $cache->setMockTime( $now ); @@ -1656,7 +1723,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { $now += 30; $cache = new WANObjectCache( [ 'cache' => $bag, - 'pool' => 'testcache-hash', 'epoch' => $now + 3600 ] ); $cache->setMockTime( $now ); @@ -1742,9 +1808,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { ->willReturn( 'special' ); $wanCache = new WANObjectCache( [ - 'cache' => $backend, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ) + 'cache' => $backend ] ); $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) ); @@ -1760,9 +1824,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { ->willReturn( 'special' ); $wanCache = new WANObjectCache( [ - 'cache' => $backend, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ) + 'cache' => $backend ] ); $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) ); @@ -1779,16 +1841,14 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase { /** * @dataProvider statsKeyProvider - * @covers WANObjectCache::determineKeyClass + * @covers WANObjectCache::determineKeyClassForStats */ public function testStatsKeyClass( $key, $class ) { $wanCache = TestingAccessWrapper::newFromObject( new WANObjectCache( [ - 'cache' => new HashBagOStuff, - 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ) + 'cache' => new HashBagOStuff ] ) ); - $this->assertEquals( $class, $wanCache->determineKeyClass( $key ) ); + $this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) ); } } diff --git a/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php b/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php index e130b23cff..33e5c3b3fb 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php @@ -44,7 +44,27 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase { ->disableOriginalConstructor() ->getMock(); - $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); + $open = true; + $db->method( 'select' )->willReturnCallback( function () use ( &$open ) { + if ( !$open ) { + throw new LogicException( "Not open" ); + } + + return new FakeResultWrapper( [] ); + } ); + $db->method( 'close' )->willReturnCallback( function () use ( &$open ) { + $open = false; + + return true; + } ); + $db->method( 'isOpen' )->willReturnCallback( function () use ( &$open ) { + return $open; + } ); + $db->method( 'open' )->willReturnCallback( function () use ( &$open ) { + $open = true; + + return $open; + } ); $db->method( '__toString' )->willReturn( 'MOCK_DB' ); return $db; @@ -55,12 +75,12 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase { */ private function getDBConnRef( ILoadBalancer $lb = null ) { $lb = $lb ?: $this->getLoadBalancerMock(); - return new DBConnRef( $lb, $this->getDatabaseMock() ); + return new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER ); } public function testConstruct() { $lb = $this->getLoadBalancerMock(); - $ref = new DBConnRef( $lb, $this->getDatabaseMock() ); + $ref = new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER ); $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) ); } @@ -79,10 +99,19 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase { $ref = new DBConnRef( $lb, - [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ] + [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ], + DB_MASTER ); $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) ); + $this->assertEquals( DB_MASTER, $ref->getReferenceRole() ); + + $ref2 = new DBConnRef( + $lb, + [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ], + DB_REPLICA + ); + $this->assertEquals( DB_REPLICA, $ref2->getReferenceRole() ); } public function testDestruct() { @@ -104,7 +133,7 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase { $this->setExpectedException( InvalidArgumentException::class, '' ); $lb = $this->getLoadBalancerMock(); - new DBConnRef( $lb, 17 ); // bad constructor argument + new DBConnRef( $lb, 17, DB_REPLICA ); // bad constructor argument } /** @@ -117,11 +146,14 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase { $lb->expects( $this->never() ) ->method( 'getConnection' ); - $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] ); + $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA ); $this->assertSame( 'dummy', $ref->getDomainID() ); } + /** + * @covers Wikimedia\Rdbms\DBConnRef::select + */ public function testSelect() { // select should get passed through normally $ref = $this->getDBConnRef(); @@ -133,8 +165,59 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase { $this->assertInternalType( 'string', $ref->__toString() ); $lb = $this->getLoadBalancerMock(); - $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ] ); + $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ], DB_MASTER ); $this->assertInternalType( 'string', $ref->__toString() ); } + /** + * @covers Wikimedia\Rdbms\DBConnRef::close + * @expectedException \Wikimedia\Rdbms\DBUnexpectedError + */ + public function testClose() { + $lb = $this->getLoadBalancerMock(); + $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_MASTER ); + $ref->close(); + } + + /** + * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole + */ + public function testGetReferenceRole() { + $lb = $this->getLoadBalancerMock(); + $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA ); + $this->assertSame( DB_REPLICA, $ref->getReferenceRole() ); + + $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'dummy', 0 ], DB_MASTER ); + $this->assertSame( DB_MASTER, $ref->getReferenceRole() ); + + $ref = new DBConnRef( $lb, [ 1, [], 'dummy', 0 ], DB_REPLICA ); + $this->assertSame( DB_REPLICA, $ref->getReferenceRole() ); + + $ref = new DBConnRef( $lb, [ 0, [], 'dummy', 0 ], DB_MASTER ); + $this->assertSame( DB_MASTER, $ref->getReferenceRole() ); + } + + /** + * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole + * @expectedException Wikimedia\Rdbms\DBReadOnlyRoleError + * @dataProvider provideRoleExceptions + */ + public function testRoleExceptions( $method, $args ) { + $lb = $this->getLoadBalancerMock(); + $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA ); + $ref->$method( ...$args ); + } + + function provideRoleExceptions() { + return [ + [ 'insert', [ 'table', [ 'a' => 1 ] ] ], + [ 'update', [ 'table', [ 'a' => 1 ], [ 'a' => 2 ] ] ], + [ 'delete', [ 'table', [ 'a' => 1 ] ] ], + [ 'replace', [ 'table', [ 'a' ], [ 'a' => 1 ] ] ], + [ 'upsert', [ 'table', [ 'a' => 1 ], [ 'a' ], [ 'a = a + 1' ] ] ], + [ 'lock', [ 'k', 'method' ] ], + [ 'unlock', [ 'k', 'method' ] ], + [ 'getScopedLockAndFlush', [ 'k', 'method', 1 ] ] + ]; + } } diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php index b36fe11f85..b1d4fadb7d 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php @@ -13,7 +13,7 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { public static function provideConstruct() { return [ 'All strings' => - [ 'foo', 'bar', 'baz', 'foo-bar-baz' ], + [ 'foo', 'bar', 'baz_', 'foo-bar-baz_' ], 'Nothing' => [ null, null, '', '' ], 'Invalid $database' => @@ -23,9 +23,9 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { 'Invalid $prefix' => [ 'foo', 'bar', 0, '', true ], 'Dash' => - [ 'foo-bar', 'baz', 'baa', 'foo?hbar-baz-baa' ], + [ 'foo-bar', 'baz', 'baa_', 'foo?hbar-baz-baa_' ], 'Question mark' => - [ 'foo?bar', 'baz', 'baa', 'foo??bar-baz-baa' ], + [ 'foo?bar', 'baz', 'baa_', 'foo??bar-baz-baa_' ], ]; } @@ -53,17 +53,17 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { 'Basic' => [ 'foo', 'foo', null, '' ], 'db+prefix' => - [ 'foo-bar', 'foo', null, 'bar' ], + [ 'foo-bar_', 'foo', null, 'bar_' ], 'db+schema+prefix' => - [ 'foo-bar-baz', 'foo', 'bar', 'baz' ], + [ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ], '?h -> -' => - [ 'foo?hbar-baz-baa', 'foo-bar', 'baz', 'baa' ], + [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ], '?? -> ?' => - [ 'foo??bar-baz-baa', 'foo?bar', 'baz', 'baa' ], + [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ], '? is left alone' => - [ 'foo?bar-baz-baa', 'foo?bar', 'baz', 'baa' ], + [ 'foo?bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ], 'too many parts' => - [ 'foo-bar-baz-baa', '', '', '', true ], + [ 'foo-bar-baz-baa_', '', '', '', true ], 'from instance' => [ DatabaseDomain::newUnspecified(), null, null, '' ], ]; @@ -90,13 +90,13 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { 'Basic' => [ 'foo', 'foo', null, '' ], 'db+prefix' => - [ 'foo-bar', 'foo', null, 'bar' ], + [ 'foo-bar_', 'foo', null, 'bar_' ], 'db+schema+prefix' => - [ 'foo-bar-baz', 'foo', 'bar', 'baz' ], + [ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ], '?h -> -' => - [ 'foo?hbar-baz-baa', 'foo-bar', 'baz', 'baa' ], + [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ], '?? -> ?' => - [ 'foo??bar-baz-baa', 'foo?bar', 'baz', 'baa' ], + [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ], 'Nothing' => [ '', null, null, '' ], ]; @@ -136,23 +136,21 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { 'Basic' => [ 'foo', 'foo', null, '', true ], 'db+prefix' => - [ 'foo-bar', 'foo', null, 'bar', true ], + [ 'foo-bar_', 'foo', null, 'bar_', true ], 'db+schema+prefix' => - [ 'foo-bar-baz', 'foo', 'bar', 'baz', true ], + [ 'foo-bar-baz_', 'foo', 'bar', 'baz_', true ], 'db+dontcare_schema+prefix' => - [ 'foo-bar-baz', 'foo', null, 'baz', false ], + [ 'foo-bar-baz_', 'foo', null, 'baz_', false ], '?h -> -' => - [ 'foo?hbar-baz-baa', 'foo-bar', 'baz', 'baa', true ], + [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_', true ], '?? -> ?' => - [ 'foo??bar-baz-baa', 'foo?bar', 'baz', 'baa', true ], + [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_', true ], 'Nothing' => [ '', null, null, '', true ], 'dontcaredb+dontcaredbschema+prefix' => - [ 'mywiki-mediawiki-prefix', null, null, 'prefix', false ], - 'dontcaredb+schema+prefix' => - [ 'mywiki-schema-prefix', null, 'schema', 'prefix', false ], + [ 'mywiki-mediawiki-prefix_', null, null, 'prefix_', false ], 'db+dontcareschema+prefix' => - [ 'mywiki-schema-prefix', 'mywiki', null, 'prefix', false ], + [ 'mywiki-schema-prefix_', 'mywiki', null, 'prefix_', false ], 'postgres-db-jobqueue' => [ 'postgres-mediawiki-', 'postgres', null, '', false ] ]; @@ -178,13 +176,11 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { public static function provideIsCompatible2() { return [ 'db+schema+prefix' => - [ 'mywiki-schema-prefix', 'thatwiki', 'schema', 'prefix' ], + [ 'mywiki-schema-prefix_', 'thatwiki', 'schema', 'prefix_' ], 'dontcaredb+dontcaredbschema+prefix' => - [ 'thatwiki-mediawiki-otherprefix', null, null, 'prefix' ], - 'dontcaredb+schema+prefix' => - [ 'mywiki-otherschema-prefix', null, 'schema', 'prefix' ], + [ 'thatwiki-mediawiki-otherprefix_', null, null, 'prefix_' ], 'db+dontcareschema+prefix' => - [ 'notmywiki-schema-prefix', 'mywiki', null, 'prefix' ], + [ 'notmywiki-schema-prefix_', 'mywiki', null, 'prefix_' ], ]; } @@ -202,6 +198,20 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase { $this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' ); } + /** + * @expectedException InvalidArgumentException + */ + public function testSchemaWithNoDB1() { + new DatabaseDomain( null, 'schema', '' ); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSchemaWithNoDB2() { + DatabaseDomain::newFromId( '-schema-prefix' ); + } + /** * @covers Wikimedia\Rdbms\DatabaseDomain::isUnspecified */ diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php index b28a5b9eec..414042ddcf 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php @@ -1,5 +1,6 @@ buildSubstring( 'foo', $start, $length ); } + /** + * @covers \Wikimedia\Rdbms\DatabaseMssql::getAttributes + */ + public function testAttributes() { + $this->assertTrue( DatabaseMssql::getAttributes()[Database::ATTR_SCHEMAS_AS_TABLE_GROUPS] ); + } } diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php index b7dbe0bd6e..4c92545128 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php @@ -672,7 +672,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { $this->assertSame( 'CAST( fieldName AS SIGNED )', $output ); } - /* + /** * @covers Wikimedia\Rdbms\Database::setIndexAliases */ public function testIndexAliases() { diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php index 4a9603c816..c0d25553dc 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php @@ -311,8 +311,8 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { /** * @covers Wikimedia\Rdbms\Subquery * @dataProvider provideSelectRowCount - * @param $sql - * @param $sqlText + * @param array $sql + * @param string $sqlText */ public function testSelectRowCount( $sql, $sqlText ) { $this->database->selectRowCount( @@ -740,6 +740,10 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { ]; } + /** + * @covers Wikimedia\Rdbms\Database::insertSelect + * @covers Wikimedia\Rdbms\Database::nativeInsertSelect + */ public function testInsertSelectBatching() { $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] ); $rows = []; @@ -1339,7 +1343,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { } /** - * @covers Wikimedia\Rdbms\Database::registerTempTableOperation + * @covers Wikimedia\Rdbms\Database::registerTempTableWrite */ public function testSessionTempTables() { $temp1 = $this->database->tableName( 'tmp_table_1' ); @@ -1849,7 +1853,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { } catch ( DBUnexpectedError $ex ) { $this->assertSame( 'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' . - __METHOD__ . 'X' . ').', + __METHOD__ . 'X).', $ex->getMessage() ); } @@ -1874,6 +1878,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { /** * @expectedException \Wikimedia\Rdbms\DBTransactionStateError + * @covers \Wikimedia\Rdbms\Database::assertTransactionStatus */ public function testTransactionErrorState1() { $wrapper = TestingAccessWrapper::newFromObject( $this->database ); diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php index bd1c1126c9..8b24791ca6 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php @@ -13,6 +13,8 @@ use Wikimedia\Rdbms\DatabaseMssql; use Wikimedia\Rdbms\DBUnexpectedError; class DatabaseTest extends PHPUnit\Framework\TestCase { + /** @var DatabaseTestHelper */ + private $db; use MediaWikiCoversValidator; @@ -629,25 +631,45 @@ class DatabaseTest extends PHPUnit\Framework\TestCase { * @covers Wikimedia\Rdbms\Database::dbSchema */ public function testSchemaAndPrefixMutators() { + $ud = DatabaseDomain::newUnspecified(); + + $this->assertEquals( $ud->getId(), $this->db->getDomainID() ); + $old = $this->db->tablePrefix(); $oldDomain = $this->db->getDomainId(); $this->assertInternalType( 'string', $old, 'Prefix is string' ); $this->assertSame( $old, $this->db->tablePrefix(), "Prefix unchanged" ); - $this->assertSame( $old, $this->db->tablePrefix( 'xxx' ) ); - $this->assertSame( 'xxx', $this->db->tablePrefix(), "Prefix set" ); + $this->assertSame( $old, $this->db->tablePrefix( 'xxx_' ) ); + $this->assertSame( 'xxx_', $this->db->tablePrefix(), "Prefix set" ); $this->db->tablePrefix( $old ); - $this->assertNotEquals( 'xxx', $this->db->tablePrefix() ); + $this->assertNotEquals( 'xxx_', $this->db->tablePrefix() ); $this->assertSame( $oldDomain, $this->db->getDomainId() ); $old = $this->db->dbSchema(); $oldDomain = $this->db->getDomainId(); $this->assertInternalType( 'string', $old, 'Schema is string' ); $this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" ); + + $this->db->selectDB( 'y' ); $this->assertSame( $old, $this->db->dbSchema( 'xxx' ) ); $this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" ); $this->db->dbSchema( $old ); $this->assertNotEquals( 'xxx', $this->db->dbSchema() ); - $this->assertSame( $oldDomain, $this->db->getDomainId() ); + $this->assertSame( "y", $this->db->getDomainId() ); + } + + /** + * @covers Wikimedia\Rdbms\Database::tablePrefix + * @covers Wikimedia\Rdbms\Database::dbSchema + * @expectedException DBUnexpectedError + */ + public function testSchemaWithNoDB() { + $ud = DatabaseDomain::newUnspecified(); + + $this->assertEquals( $ud->getId(), $this->db->getDomainID() ); + $this->assertSame( '', $this->db->dbSchema() ); + + $this->db->dbSchema( 'xxx' ); } /** @@ -659,10 +681,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase { $oldSchema = $this->db->dbSchema(); $oldPrefix = $this->db->tablePrefix(); - $this->db->selectDomain( 'testselectdb-xxx' ); + $this->db->selectDomain( 'testselectdb-xxx_' ); $this->assertSame( 'testselectdb', $this->db->getDBname() ); $this->assertSame( '', $this->db->dbSchema() ); - $this->assertSame( 'xxx', $this->db->tablePrefix() ); + $this->assertSame( 'xxx_', $this->db->tablePrefix() ); $this->db->selectDomain( $oldDomain ); $this->assertSame( $oldDatabase, $this->db->getDBname() ); @@ -670,10 +692,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase { $this->assertSame( $oldPrefix, $this->db->tablePrefix() ); $this->assertSame( $oldDomain, $this->db->getDomainId() ); - $this->db->selectDomain( 'testselectdb-schema-xxx' ); + $this->db->selectDomain( 'testselectdb-schema-xxx_' ); $this->assertSame( 'testselectdb', $this->db->getDBname() ); $this->assertSame( 'schema', $this->db->dbSchema() ); - $this->assertSame( 'xxx', $this->db->tablePrefix() ); + $this->assertSame( 'xxx_', $this->db->tablePrefix() ); $this->db->selectDomain( $oldDomain ); $this->assertSame( $oldDatabase, $this->db->getDBname() ); diff --git a/tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php b/tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php index b55d8697db..46e23e36db 100644 --- a/tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php +++ b/tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php @@ -31,7 +31,7 @@ class PrefixingStatsdDataFactoryProxyTest extends PHPUnit\Framework\TestCase { ); $innerFactory->expects( $this->once() ) ->method( $method ) - ->with( 'testprefix.' . 'metricname' ); + ->with( 'testprefix.metricname' ); $proxy = new PrefixingStatsdDataFactoryProxy( $innerFactory, 'testprefix' ); // 1,2,3,4 simply makes sure we provide enough parameters, without caring what they are @@ -48,7 +48,7 @@ class PrefixingStatsdDataFactoryProxyTest extends PHPUnit\Framework\TestCase { ); $innerFactory->expects( $this->once() ) ->method( $method ) - ->with( 'testprefix.' . 'metricname' ); + ->with( 'testprefix.metricname' ); $proxy = new PrefixingStatsdDataFactoryProxy( $innerFactory, 'testprefix...' ); // 1,2,3,4 simply makes sure we provide enough parameters, without caring what they are diff --git a/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php b/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php index ad0c3d1eae..e3a200b2e9 100644 --- a/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php +++ b/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php @@ -4,7 +4,7 @@ * @covers PageDataRequestHandler * @group PageData */ -class PageDataRequestHandlerTest extends \MediaWikiTestCase { +class PageDataRequestHandlerTest extends \MediaWikiLangTestCase { /** * @var Title @@ -19,9 +19,10 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { protected function setUp() { parent::setUp(); - $this->interfaceTitle = Title::newFromText( "Special:PageDataRequestHandlerTest" ); - + $this->interfaceTitle = Title::newFromText( __CLASS__ ); $this->obLevel = ob_get_level(); + + $this->setMwGlobals( 'wgArticlePath', '/wiki/$1' ); } protected function tearDown() { @@ -44,7 +45,7 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { * @return PageDataRequestHandler */ protected function newHandler() { - return new PageDataRequestHandler( 'json' ); + return new PageDataRequestHandler(); } /** @@ -76,9 +77,16 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { public function handleRequestProvider() { $cases = []; - $cases[] = [ '', [], [], '!!', 400 ]; + $cases[] = [ '', [], [], 'Invalid title', 400 ]; - $cases[] = [ '', [ 'target' => 'Helsinki' ], [], '!!', 303, [ 'Location' => '!.+!' ] ]; + $cases[] = [ + '', + [ 'target' => 'Helsinki' ], + [], + '', + 303, + [ 'Location' => '?title=Helsinki&action=raw' ] + ]; $subpageCases = []; foreach ( $cases as $c ) { @@ -99,9 +107,9 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { '', [ 'target' => 'Helsinki' ], [ 'Accept' => 'text/HTML' ], - '!!', + '', 303, - [ 'Location' => '!Helsinki$!' ] + [ 'Location' => '/wiki/Helsinki' ] ]; $cases[] = [ @@ -111,18 +119,18 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { 'revision' => '4242', ], [ 'Accept' => 'text/HTML' ], - '!!', + '', 303, - [ 'Location' => '!Helsinki(\?|&)oldid=4242!' ] + [ 'Location' => '?title=Helsinki&oldid=4242' ] ]; $cases[] = [ '/Helsinki', [], [], - '!!', + '', 303, - [ 'Location' => '!Helsinki&action=raw!' ] + [ 'Location' => '?title=Helsinki&action=raw' ] ]; // #31: /Q5 with "Accept: text/foobar" triggers a 406 @@ -130,36 +138,59 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { 'main/Helsinki', [], [ 'Accept' => 'text/foobar' ], - '!!', + 'No matching format found', 406, + ]; + + $cases[] = [ + 'no slash', [], + [ 'Accept' => 'text/HTML' ], + 'Invalid title', + 400, + ]; + + $cases[] = [ + 'main', + [], + [ 'Accept' => 'text/HTML' ], + 'Invalid title', + 400, + ]; + + $cases[] = [ + 'xyz/Helsinki', + [], + [ 'Accept' => 'text/HTML' ], + 'Invalid title', + 400, ]; $cases[] = [ 'main/Helsinki', [], [ 'Accept' => 'text/HTML' ], - '!!', + '', 303, - [ 'Location' => '!Helsinki$!' ] + [ 'Location' => '/wiki/Helsinki' ] ]; $cases[] = [ '/Helsinki', [], [ 'Accept' => 'text/HTML' ], - '!!', + '', 303, - [ 'Location' => '!Helsinki$!' ] + [ 'Location' => '/wiki/Helsinki' ] ]; $cases[] = [ 'main/AC/DC', [], [ 'Accept' => 'text/HTML' ], - '!!', + '', 303, - [ 'Location' => '!AC/DC$!' ] + [ 'Location' => '/wiki/AC/DC' ] ]; return $cases; @@ -171,7 +202,7 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { * @param string $subpage The subpage to request (or '') * @param array $params Request parameters * @param array $headers Request headers - * @param string $expectedOutput Regex to match the output against. + * @param string $expectedOutput * @param int $expectedStatusCode Expected HTTP status code. * @param string[] $expectedHeaders Expected HTTP response headers. */ @@ -179,7 +210,7 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { $subpage, array $params, array $headers, - $expectedOutput, + $expectedOutput = '', $expectedStatusCode = 200, array $expectedHeaders = [] ) { @@ -201,22 +232,21 @@ class PageDataRequestHandlerTest extends \MediaWikiTestCase { $output->output(); } - $text = ob_get_contents(); - ob_end_clean(); + $text = ob_get_clean(); $this->assertEquals( $expectedStatusCode, $response->getStatusCode(), 'status code' ); - $this->assertRegExp( $expectedOutput, $text, 'output' ); + $this->assertSame( $expectedOutput, $text, 'output' ); foreach ( $expectedHeaders as $name => $exp ) { $value = $response->getHeader( $name ); $this->assertNotNull( $value, "header: $name" ); $this->assertInternalType( 'string', $value, "header: $name" ); - $this->assertRegExp( $exp, $value, "header: $name" ); + $this->assertStringEndsWith( $exp, $value, "header: $name" ); } } catch ( HttpError $e ) { ob_end_clean(); $this->assertEquals( $expectedStatusCode, $e->getStatusCode(), 'status code' ); - $this->assertRegExp( $expectedOutput, $e->getHTML(), 'error output' ); + $this->assertContains( $expectedOutput, $e->getHTML(), 'error output' ); } // We always set "Access-Control-Allow-Origin: *" diff --git a/tests/phpunit/includes/linker/LinkRendererTest.php b/tests/phpunit/includes/linker/LinkRendererTest.php index d550dcbe26..91ee276550 100644 --- a/tests/phpunit/includes/linker/LinkRendererTest.php +++ b/tests/phpunit/includes/linker/LinkRendererTest.php @@ -51,11 +51,10 @@ class LinkRendererTest extends MediaWikiLangTestCase { // Query added $this->assertEquals( - 'Foobar', + 'Foobar', $linkRenderer->makeKnownLink( $target, null, [], [ 'foo' => 'bar' ] ) ); - // forcearticlepath $linkRenderer->setForceArticlePath( true ); $this->assertEquals( 'Foobar', diff --git a/tests/phpunit/includes/logging/DatabaseLogEntryTest.php b/tests/phpunit/includes/logging/DatabaseLogEntryTest.php index e75b1739d2..b183cde1d2 100644 --- a/tests/phpunit/includes/logging/DatabaseLogEntryTest.php +++ b/tests/phpunit/includes/logging/DatabaseLogEntryTest.php @@ -29,18 +29,15 @@ class DatabaseLogEntryTest extends MediaWikiTestCase { * @param array $selectFields * @param string[]|null $row * @param string[]|null $expectedFields - * @param int $commentMigration * @param int $actorMigration */ public function testNewFromId( $id, array $selectFields, array $row = null, array $expectedFields = null, - $commentMigration, $actorMigration ) { $this->setMwGlobals( [ - 'wgCommentTableSchemaMigrationStage' => $commentMigration, 'wgActorTableSchemaMigrationStage' => $actorMigration, ] ); @@ -71,7 +68,10 @@ class DatabaseLogEntryTest extends MediaWikiTestCase { public function provideNewFromId() { $oldTables = [ - 'tables' => [ 'logging', 'user' ], + 'tables' => [ + 'logging', 'user', + 'comment_log_comment' => 'comment', + ], 'fields' => [ 'log_id', 'log_type', @@ -84,15 +84,18 @@ class DatabaseLogEntryTest extends MediaWikiTestCase { 'user_id', 'user_name', 'user_editcount', - 'log_comment_text' => 'log_comment', - 'log_comment_data' => 'NULL', - 'log_comment_cid' => 'NULL', + 'log_comment_text' => 'comment_log_comment.comment_text', + 'log_comment_data' => 'comment_log_comment.comment_data', + 'log_comment_cid' => 'comment_log_comment.comment_id', 'log_user' => 'log_user', 'log_user_text' => 'log_user_text', 'log_actor' => 'NULL', ], 'options' => [], - 'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id=log_user' ] ], + 'join_conds' => [ + 'user' => [ 'LEFT JOIN', 'user_id=log_user' ], + 'comment_log_comment' => [ 'JOIN', 'comment_log_comment.comment_id = log_comment_id' ], + ], ]; $newTables = [ 'tables' => [ @@ -133,7 +136,6 @@ class DatabaseLogEntryTest extends MediaWikiTestCase { $oldTables + [ 'conds' => [ 'log_id' => 0 ] ], null, null, - MIGRATION_OLD, SCHEMA_COMPAT_OLD, ], [ @@ -146,7 +148,6 @@ class DatabaseLogEntryTest extends MediaWikiTestCase { 'log_comment_data' => null, ], [ 'type' => 'foobarize', 'comment' => 'test!' ], - MIGRATION_OLD, SCHEMA_COMPAT_OLD, ], [ @@ -159,7 +160,6 @@ class DatabaseLogEntryTest extends MediaWikiTestCase { 'log_comment_data' => null, ], [ 'type' => 'foobarize', 'comment' => 'test!' ], - MIGRATION_NEW, SCHEMA_COMPAT_NEW, ], ]; diff --git a/tests/phpunit/includes/logging/UploadLogFormatterTest.php b/tests/phpunit/includes/logging/UploadLogFormatterTest.php index 2b4067f17d..b393949424 100644 --- a/tests/phpunit/includes/logging/UploadLogFormatterTest.php +++ b/tests/phpunit/includes/logging/UploadLogFormatterTest.php @@ -134,7 +134,7 @@ class UploadLogFormatterTest extends LogFormatterTestCase { ], ], [ - 'text' => 'User uploaded File:File.png', + 'text' => 'User reverted File:File.png to an old version', 'api' => [ 'img_sha1' => 'hash', 'img_timestamp' => '2015-01-01T00:00:00Z', @@ -153,7 +153,7 @@ class UploadLogFormatterTest extends LogFormatterTestCase { 'params' => [], ], [ - 'text' => 'User uploaded File:File.png', + 'text' => 'User reverted File:File.png to an old version', 'api' => [], ], ], diff --git a/tests/phpunit/includes/media/DjVuTest.php b/tests/phpunit/includes/media/DjVuTest.php index dbc0d2fbd5..d9b5d824dd 100644 --- a/tests/phpunit/includes/media/DjVuTest.php +++ b/tests/phpunit/includes/media/DjVuTest.php @@ -25,7 +25,7 @@ class DjVuTest extends MediaWikiMediaTestCase { } public function testGetImageSize() { - $this->assertArrayEquals( + $this->assertSame( [ 2480, 3508, 'DjVu', 'width="2480" height="3508"' ], $this->handler->getImageSize( null, $this->filePath . '/LoremIpsum.djvu' ), 'Test file LoremIpsum.djvu should have a size of 2480 * 3508' @@ -51,8 +51,8 @@ class DjVuTest extends MediaWikiMediaTestCase { public function testGetPageDimensions() { $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' ); - $this->assertArrayEquals( - [ 2480, 3508 ], + $this->assertSame( + [ 'width' => 2480, 'height' => 3508 ], $this->handler->getPageDimensions( $file, 1 ), 'Page 1 of test file LoremIpsum.djvu should have a size of 2480 * 3508' ); diff --git a/tests/phpunit/includes/media/GIFHandlerTest.php b/tests/phpunit/includes/media/GIFHandlerTest.php new file mode 100644 index 0000000000..4dd7443e48 --- /dev/null +++ b/tests/phpunit/includes/media/GIFHandlerTest.php @@ -0,0 +1,172 @@ +handler = new GIFHandler(); + } + + /** + * @covers GIFHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); + $this->assertEquals( GIFHandler::BROKEN_FILE, $res ); + } + + /** + * @param string $filename Basename of the file to check + * @param bool $expected Expected result. + * @dataProvider provideIsAnimated + * @covers GIFHandler::isAnimatedImage + */ + public function testIsAnimanted( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsAnimated() { + return [ + [ 'animated.gif', true ], + [ 'nonanimated.gif', false ], + ]; + } + + /** + * @param string $filename + * @param int $expected Total image area + * @dataProvider provideGetImageArea + * @covers GIFHandler::getImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetImageArea() { + return [ + [ 'animated.gif', 5400 ], + [ 'nonanimated.gif', 1350 ], + ]; + } + + /** + * @param string $metadata Serialized metadata + * @param int $expected One of the class constants of GIFHandler + * @dataProvider provideIsMetadataValid + * @covers GIFHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + // phpcs:disable Generic.Files.LineLength + return [ + [ GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ], + [ '', GIFHandler::METADATA_BAD ], + [ null, GIFHandler::METADATA_BAD ], + [ 'Something invalid!', GIFHandler::METADATA_BAD ], + [ + 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', + GIFHandler::METADATA_GOOD + ], + ]; + // phpcs:enable + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetMetadata + * @covers GIFHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); + $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + } + + public static function provideGetMetadata() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + 'nonanimated.gif', + 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' + ], + [ + 'animated-xmp.gif', + 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' + ], + ]; + // phpcs:enable + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetIndependentMetaArray + * @covers GIFHandler::getCommonMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getCommonMetaArray( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetIndependentMetaArray() { + return [ + [ 'nonanimated.gif', [ + 'GIFFileComment' => [ + 'GIF test file ⁕ Created with GIMP', + ], + ] ], + [ 'animated-xmp.gif', + [ + 'Artist' => 'Bawolff', + 'ImageDescription' => [ + 'x-default' => 'A file to test GIF', + '_type' => 'lang', + ], + 'SublocationDest' => 'The interwebs', + 'GIFFileComment' => + [ + 'GIƒ·test·file', + ], + ] + ], + ]; + } + + /** + * @param string $filename + * @param float $expectedLength + * @dataProvider provideGetLength + * @covers GIFHandler::getLength + */ + public function testGetLength( $filename, $expectedLength ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actualLength = $file->getLength(); + $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 ); + } + + public function provideGetLength() { + return [ + [ 'animated.gif', 2.4 ], + [ 'animated-xmp.gif', 2.4 ], + [ 'nonanimated', 0.0 ], + [ 'Bishzilla_blink.gif', 1.4 ], + ]; + } +} diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php deleted file mode 100644 index 4dd7443e48..0000000000 --- a/tests/phpunit/includes/media/GIFTest.php +++ /dev/null @@ -1,172 +0,0 @@ -handler = new GIFHandler(); - } - - /** - * @covers GIFHandler::getMetadata - */ - public function testInvalidFile() { - $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); - $this->assertEquals( GIFHandler::BROKEN_FILE, $res ); - } - - /** - * @param string $filename Basename of the file to check - * @param bool $expected Expected result. - * @dataProvider provideIsAnimated - * @covers GIFHandler::isAnimatedImage - */ - public function testIsAnimanted( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/gif' ); - $actual = $this->handler->isAnimatedImage( $file ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideIsAnimated() { - return [ - [ 'animated.gif', true ], - [ 'nonanimated.gif', false ], - ]; - } - - /** - * @param string $filename - * @param int $expected Total image area - * @dataProvider provideGetImageArea - * @covers GIFHandler::getImageArea - */ - public function testGetImageArea( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/gif' ); - $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideGetImageArea() { - return [ - [ 'animated.gif', 5400 ], - [ 'nonanimated.gif', 1350 ], - ]; - } - - /** - * @param string $metadata Serialized metadata - * @param int $expected One of the class constants of GIFHandler - * @dataProvider provideIsMetadataValid - * @covers GIFHandler::isMetadataValid - */ - public function testIsMetadataValid( $metadata, $expected ) { - $actual = $this->handler->isMetadataValid( null, $metadata ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideIsMetadataValid() { - // phpcs:disable Generic.Files.LineLength - return [ - [ GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ], - [ '', GIFHandler::METADATA_BAD ], - [ null, GIFHandler::METADATA_BAD ], - [ 'Something invalid!', GIFHandler::METADATA_BAD ], - [ - 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', - GIFHandler::METADATA_GOOD - ], - ]; - // phpcs:enable - } - - /** - * @param string $filename - * @param string $expected Serialized array - * @dataProvider provideGetMetadata - * @covers GIFHandler::getMetadata - */ - public function testGetMetadata( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/gif' ); - $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); - $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); - } - - public static function provideGetMetadata() { - // phpcs:disable Generic.Files.LineLength - return [ - [ - 'nonanimated.gif', - 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' - ], - [ - 'animated-xmp.gif', - 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' - ], - ]; - // phpcs:enable - } - - /** - * @param string $filename - * @param string $expected Serialized array - * @dataProvider provideGetIndependentMetaArray - * @covers GIFHandler::getCommonMetaArray - */ - public function testGetIndependentMetaArray( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/gif' ); - $actual = $this->handler->getCommonMetaArray( $file ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideGetIndependentMetaArray() { - return [ - [ 'nonanimated.gif', [ - 'GIFFileComment' => [ - 'GIF test file ⁕ Created with GIMP', - ], - ] ], - [ 'animated-xmp.gif', - [ - 'Artist' => 'Bawolff', - 'ImageDescription' => [ - 'x-default' => 'A file to test GIF', - '_type' => 'lang', - ], - 'SublocationDest' => 'The interwebs', - 'GIFFileComment' => - [ - 'GIƒ·test·file', - ], - ] - ], - ]; - } - - /** - * @param string $filename - * @param float $expectedLength - * @dataProvider provideGetLength - * @covers GIFHandler::getLength - */ - public function testGetLength( $filename, $expectedLength ) { - $file = $this->dataFile( $filename, 'image/gif' ); - $actualLength = $file->getLength(); - $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 ); - } - - public function provideGetLength() { - return [ - [ 'animated.gif', 2.4 ], - [ 'animated-xmp.gif', 2.4 ], - [ 'nonanimated', 0.0 ], - [ 'Bishzilla_blink.gif', 1.4 ], - ]; - } -} diff --git a/tests/phpunit/includes/media/JpegPixelFormatTest.php b/tests/phpunit/includes/media/JpegPixelFormatTest.php index 6815a62bd9..630df54335 100644 --- a/tests/phpunit/includes/media/JpegPixelFormatTest.php +++ b/tests/phpunit/includes/media/JpegPixelFormatTest.php @@ -1,11 +1,12 @@ getLocalCopyPath(); $this->assertTrue( is_string( $path ), "path returned for JPEG thumbnail for $fmtStr" ); - $cmd = [ - 'identify', + $result = Shell::command( 'identify', '-format', '%[jpeg:sampling-factor]', $path - ]; - $retval = null; - $output = wfShellExec( $cmd, $retval ); - $this->assertTrue( $retval === 0, "ImageMagick's identify command should return success" ); + )->execute(); + $this->assertEquals( 0, + $result->getExitCode(), + "ImageMagick's identify command should return success" + ); $expected = $samplingFactor; - $actual = trim( $output ); + $actual = trim( $result->getStdout() ); $this->assertEquals( $expected, - trim( $output ), + $actual, "IM identify expects JPEG chroma subsampling \"$expected\" for $fmtStr" ); } diff --git a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php index 7a536df944..2e9acfade4 100644 --- a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php +++ b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php @@ -70,16 +70,10 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { * * File must be in the path returned by getFilePath() * @param string $name File name - * @param string|null $type MIME type [optional] + * @param string|false $type MIME type [optional] * @return UnregisteredLocalFile */ - protected function dataFile( $name, $type = null ) { - if ( !$type ) { - // Autodetect by file extension for the lazy. - $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); - $parts = explode( $name, '.' ); - $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] ); - } + protected function dataFile( $name, $type = false ) { return new UnregisteredLocalFile( false, $this->repo, "mwstore://localtesting/data/$name", $type ); } diff --git a/tests/phpunit/includes/media/PNGHandlerTest.php b/tests/phpunit/includes/media/PNGHandlerTest.php new file mode 100644 index 0000000000..5a66586e25 --- /dev/null +++ b/tests/phpunit/includes/media/PNGHandlerTest.php @@ -0,0 +1,161 @@ +handler = new PNGHandler(); + } + + /** + * @covers PNGHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); + $this->assertEquals( PNGHandler::BROKEN_FILE, $res ); + } + + /** + * @param string $filename Basename of the file to check + * @param bool $expected Expected result. + * @dataProvider provideIsAnimated + * @covers PNGHandler::isAnimatedImage + */ + public function testIsAnimanted( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsAnimated() { + return [ + [ 'Animated_PNG_example_bouncing_beach_ball.png', true ], + [ '1bit-png.png', false ], + ]; + } + + /** + * @param string $filename + * @param int $expected Total image area + * @dataProvider provideGetImageArea + * @covers PNGHandler::getImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetImageArea() { + return [ + [ '1bit-png.png', 2500 ], + [ 'greyscale-png.png', 2500 ], + [ 'Png-native-test.png', 126000 ], + [ 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ], + ]; + } + + /** + * @param string $metadata Serialized metadata + * @param int $expected One of the class constants of PNGHandler + * @dataProvider provideIsMetadataValid + * @covers PNGHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + // phpcs:disable Generic.Files.LineLength + return [ + [ PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ], + [ '', PNGHandler::METADATA_BAD ], + [ null, PNGHandler::METADATA_BAD ], + [ 'Something invalid!', PNGHandler::METADATA_BAD ], + [ + 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', + PNGHandler::METADATA_GOOD + ], + ]; + // phpcs:enable + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetMetadata + * @covers PNGHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); +// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + $this->assertEquals( ( $expected ), ( $actual ) ); + } + + public static function provideGetMetadata() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + 'rgb-na-png.png', + 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' + ], + [ + 'xmp.png', + 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' + ], + ]; + // phpcs:enable + } + + /** + * @param string $filename + * @param array $expected Expected standard metadata + * @dataProvider provideGetIndependentMetaArray + * @covers PNGHandler::getCommonMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getCommonMetaArray( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetIndependentMetaArray() { + return [ + [ 'rgb-na-png.png', [] ], + [ 'xmp.png', + [ + 'SerialNumber' => '123456789', + ] + ], + ]; + } + + /** + * @param string $filename + * @param float $expectedLength + * @dataProvider provideGetLength + * @covers PNGHandler::getLength + */ + public function testGetLength( $filename, $expectedLength ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actualLength = $file->getLength(); + $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 ); + } + + public function provideGetLength() { + return [ + [ 'Animated_PNG_example_bouncing_beach_ball.png', 1.5 ], + [ 'Png-native-test.png', 0.0 ], + [ 'greyscale-png.png', 0.0 ], + [ '1bit-png.png', 0.0 ], + ]; + } +} diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php deleted file mode 100644 index 5a66586e25..0000000000 --- a/tests/phpunit/includes/media/PNGTest.php +++ /dev/null @@ -1,161 +0,0 @@ -handler = new PNGHandler(); - } - - /** - * @covers PNGHandler::getMetadata - */ - public function testInvalidFile() { - $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); - $this->assertEquals( PNGHandler::BROKEN_FILE, $res ); - } - - /** - * @param string $filename Basename of the file to check - * @param bool $expected Expected result. - * @dataProvider provideIsAnimated - * @covers PNGHandler::isAnimatedImage - */ - public function testIsAnimanted( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/png' ); - $actual = $this->handler->isAnimatedImage( $file ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideIsAnimated() { - return [ - [ 'Animated_PNG_example_bouncing_beach_ball.png', true ], - [ '1bit-png.png', false ], - ]; - } - - /** - * @param string $filename - * @param int $expected Total image area - * @dataProvider provideGetImageArea - * @covers PNGHandler::getImageArea - */ - public function testGetImageArea( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/png' ); - $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideGetImageArea() { - return [ - [ '1bit-png.png', 2500 ], - [ 'greyscale-png.png', 2500 ], - [ 'Png-native-test.png', 126000 ], - [ 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ], - ]; - } - - /** - * @param string $metadata Serialized metadata - * @param int $expected One of the class constants of PNGHandler - * @dataProvider provideIsMetadataValid - * @covers PNGHandler::isMetadataValid - */ - public function testIsMetadataValid( $metadata, $expected ) { - $actual = $this->handler->isMetadataValid( null, $metadata ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideIsMetadataValid() { - // phpcs:disable Generic.Files.LineLength - return [ - [ PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ], - [ '', PNGHandler::METADATA_BAD ], - [ null, PNGHandler::METADATA_BAD ], - [ 'Something invalid!', PNGHandler::METADATA_BAD ], - [ - 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', - PNGHandler::METADATA_GOOD - ], - ]; - // phpcs:enable - } - - /** - * @param string $filename - * @param string $expected Serialized array - * @dataProvider provideGetMetadata - * @covers PNGHandler::getMetadata - */ - public function testGetMetadata( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/png' ); - $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); -// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); - $this->assertEquals( ( $expected ), ( $actual ) ); - } - - public static function provideGetMetadata() { - // phpcs:disable Generic.Files.LineLength - return [ - [ - 'rgb-na-png.png', - 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' - ], - [ - 'xmp.png', - 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' - ], - ]; - // phpcs:enable - } - - /** - * @param string $filename - * @param array $expected Expected standard metadata - * @dataProvider provideGetIndependentMetaArray - * @covers PNGHandler::getCommonMetaArray - */ - public function testGetIndependentMetaArray( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/png' ); - $actual = $this->handler->getCommonMetaArray( $file ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideGetIndependentMetaArray() { - return [ - [ 'rgb-na-png.png', [] ], - [ 'xmp.png', - [ - 'SerialNumber' => '123456789', - ] - ], - ]; - } - - /** - * @param string $filename - * @param float $expectedLength - * @dataProvider provideGetLength - * @covers PNGHandler::getLength - */ - public function testGetLength( $filename, $expectedLength ) { - $file = $this->dataFile( $filename, 'image/png' ); - $actualLength = $file->getLength(); - $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 ); - } - - public function provideGetLength() { - return [ - [ 'Animated_PNG_example_bouncing_beach_ball.png', 1.5 ], - [ 'Png-native-test.png', 0.0 ], - [ 'greyscale-png.png', 0.0 ], - [ '1bit-png.png', 0.0 ], - ]; - } -} diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php index 7aef246fcc..6b94d0ae6c 100644 --- a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -18,11 +18,6 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { */ public function testGetXMLMetadata( $infile, $expected ) { $r = new XMLReader(); - if ( !method_exists( $r, 'readInnerXML' ) ) { - $this->markTestSkipped( 'XMLReader::readInnerXML() does not exist (libxml >2.6.20 needed).' ); - - return; - } $this->assertMetadata( $infile, $expected ); } diff --git a/tests/phpunit/includes/media/SvgHandlerTest.php b/tests/phpunit/includes/media/SvgHandlerTest.php index 9c98ada43b..bce7ac2684 100644 --- a/tests/phpunit/includes/media/SvgHandlerTest.php +++ b/tests/phpunit/includes/media/SvgHandlerTest.php @@ -182,7 +182,7 @@ class SvgHandlerTest extends MediaWikiMediaTestCase { * @covers SvgHandler::normaliseParamsInternal() * @dataProvider provideNormaliseParamsInternal * - * @param $message + * @param string $message * @param int $width * @param int $height * @param array $params diff --git a/tests/phpunit/includes/media/WebPHandlerTest.php b/tests/phpunit/includes/media/WebPHandlerTest.php new file mode 100644 index 0000000000..ac0ad98edd --- /dev/null +++ b/tests/phpunit/includes/media/WebPHandlerTest.php @@ -0,0 +1,151 @@ +tempFileName = tempnam( wfTempDir(), 'WEBP' ); + } + + public function tearDown() { + parent::tearDown(); + unlink( $this->tempFileName ); + } + + /** + * @dataProvider provideTestExtractMetaData + */ + public function testExtractMetaData( $header, $expectedResult ) { + // Put header into file + file_put_contents( $this->tempFileName, $header ); + + $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) ); + } + + public function provideTestExtractMetaData() { + // phpcs:disable Generic.Files.LineLength + return [ + // Files from https://developers.google.com/speed/webp/gallery2 + [ "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C", + [ 'compression' => 'lossless', 'width' => 400, 'height' => 301 ] ], + [ "\x52\x49\x46\x46\x64\x5B\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x8F\x01\x00\x2C\x01\x00\x41\x4C\x50\x48\xE5\x0E", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 400, 'height' => 301 ] ], + [ "\x52\x49\x46\x46\xA8\x72\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x9B\x72\x00\x00\x2F\x81\x81\x62\x10\x8D\x40\x8C\x24\x39\x6E\x73\x73\x38\x01\x96", + [ 'compression' => 'lossless', 'width' => 386, 'height' => 395 ] ], + [ "\x52\x49\x46\x46\xE0\x42\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x81\x01\x00\x8A\x01\x00\x41\x4C\x50\x48\x56\x10", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ] ], + [ "\x52\x49\x46\x46\x70\x61\x02\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x63\x61\x02\x00\x2F\x1F\xC3\x95\x10\x8D\xC8\x72\xDB\xC8\x92\x24\xD8\x91\xD9\x91", + [ 'compression' => 'lossless', 'width' => 800, 'height' => 600 ] ], + [ "\x52\x49\x46\x46\x1C\x1D\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x1F\x03\x00\x57\x02\x00\x41\x4C\x50\x48\x25\x8B", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 800, 'height' => 600 ] ], + [ "\x52\x49\x46\x46\xFA\xC5\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xEE\xC5\x00\x00\x2F\xA4\x81\x28\x10\x8D\x40\x68\x24\xC9\x91\xA4\xAE\xF3\x97\x75", + [ 'compression' => 'lossless', 'width' => 421, 'height' => 163 ] ], + [ "\x52\x49\x46\x46\xF6\x5D\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\xA4\x01\x00\xA2\x00\x00\x41\x4C\x50\x48\x38\x1A", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 421, 'height' => 163 ] ], + [ "\x52\x49\x46\x46\xC4\x96\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xB8\x96\x01\x00\x2F\x2B\xC1\x4A\x10\x11\x87\x6D\xDB\x48\x12\xFC\x60\xB0\x83\x24", + [ 'compression' => 'lossless', 'width' => 300, 'height' => 300 ] ], + [ "\x52\x49\x46\x46\x0A\x11\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x2B\x01\x00\x2B\x01\x00\x41\x4C\x50\x48\x67\x6E", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 300, 'height' => 300 ] ], + + // Lossy files from https://developers.google.com/speed/webp/gallery1 + [ "\x52\x49\x46\x46\x68\x76\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\x5C\x76\x00\x00\xD2\xBE\x01\x9D\x01\x2A\x26\x02\x70\x01\x3E\xD5\x4E\x97\x43\xA2", + [ 'compression' => 'lossy', 'width' => 550, 'height' => 368 ] ], + [ "\x52\x49\x46\x46\xB0\xEC\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\xA4\xEC\x00\x00\xB2\x4B\x02\x9D\x01\x2A\x26\x02\x94\x01\x3E\xD1\x50\x96\x46\x26", + [ 'compression' => 'lossy', 'width' => 550, 'height' => 404 ] ], + [ "\x52\x49\x46\x46\x7A\x19\x03\x00\x57\x45\x42\x50\x56\x50\x38\x20\x6E\x19\x03\x00\xB2\xF8\x09\x9D\x01\x2A\x00\x05\xD0\x02\x3E\xAD\x46\x99\x4A\xA5", + [ 'compression' => 'lossy', 'width' => 1280, 'height' => 720 ] ], + [ "\x52\x49\x46\x46\x44\xB3\x02\x00\x57\x45\x42\x50\x56\x50\x38\x20\x38\xB3\x02\x00\x52\x57\x06\x9D\x01\x2A\x00\x04\x04\x03\x3E\xA5\x44\x96\x49\x26", + [ 'compression' => 'lossy', 'width' => 1024, 'height' => 772 ] ], + [ "\x52\x49\x46\x46\x02\x43\x01\x00\x57\x45\x42\x50\x56\x50\x38\x20\xF6\x42\x01\x00\x12\xC0\x05\x9D\x01\x2A\x00\x04\xF0\x02\x3E\x79\x34\x93\x47\xA4", + [ 'compression' => 'lossy', 'width' => 1024, 'height' => 752 ] ], + + // Animated file from https://groups.google.com/a/chromium.org/d/topic/blink-dev/Y8tRC4mdQz8/discussion + [ "\x52\x49\x46\x46\xD0\x0B\x02\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x12\x00\x00\x00\x3F\x01\x00\x3F\x01\x00\x41\x4E", + [ 'compression' => 'unknown', 'animated' => true, 'transparency' => true, 'width' => 320, 'height' => 320 ] ], + + // Error cases + [ '', false ], + [ ' ', false ], + [ 'RIFF ', false ], + [ 'RIFF1234WEBP ', false ], + [ 'RIFF1234WEBPVP8 ', false ], + [ 'RIFF1234WEBPVP8L ', false ], + ]; + // phpcs:enable + } + + /** + * @dataProvider provideTestWithFileExtractMetaData + */ + public function testWithFileExtractMetaData( $filename, $expectedResult ) { + $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $filename ) ); + } + + public function provideTestWithFileExtractMetaData() { + return [ + [ __DIR__ . '/../../data/media/2_webp_ll.webp', + [ + 'compression' => 'lossless', + 'width' => 386, + 'height' => 395 + ] + ], + [ __DIR__ . '/../../data/media/2_webp_a.webp', + [ + 'compression' => 'lossy', + 'animated' => false, + 'transparency' => true, + 'width' => 386, + 'height' => 395 + ] + ], + ]; + } + + /** + * @dataProvider provideTestGetImageSize + */ + public function testGetImageSize( $path, $expectedResult ) { + $handler = new WebPHandler(); + $this->assertEquals( $expectedResult, $handler->getImageSize( null, $path ) ); + } + + public function provideTestGetImageSize() { + return [ + // Public domain files from https://developers.google.com/speed/webp/gallery2 + [ __DIR__ . '/../../data/media/2_webp_a.webp', [ 386, 395 ] ], + [ __DIR__ . '/../../data/media/2_webp_ll.webp', [ 386, 395 ] ], + [ __DIR__ . '/../../data/media/webp_animated.webp', [ 300, 225 ] ], + + // Error cases + [ __FILE__, false ], + ]; + } + + /** + * Tests the WebP MIME detection. This should really be a separate test, but sticking it + * here for now. + * + * @dataProvider provideTestGetMimeType + */ + public function testGuessMimeType( $path ) { + $mime = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); + $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) ); + } + + public function provideTestGetMimeType() { + return [ + // Public domain files from https://developers.google.com/speed/webp/gallery2 + [ __DIR__ . '/../../data/media/2_webp_a.webp' ], + [ __DIR__ . '/../../data/media/2_webp_ll.webp' ], + [ __DIR__ . '/../../data/media/webp_animated.webp' ], + ]; + } +} + +/* Python code to extract a header and convert to PHP format: + * print '"%s"' % ''.implode( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) ) + */ diff --git a/tests/phpunit/includes/media/WebPTest.php b/tests/phpunit/includes/media/WebPTest.php deleted file mode 100644 index ac0ad98edd..0000000000 --- a/tests/phpunit/includes/media/WebPTest.php +++ /dev/null @@ -1,151 +0,0 @@ -tempFileName = tempnam( wfTempDir(), 'WEBP' ); - } - - public function tearDown() { - parent::tearDown(); - unlink( $this->tempFileName ); - } - - /** - * @dataProvider provideTestExtractMetaData - */ - public function testExtractMetaData( $header, $expectedResult ) { - // Put header into file - file_put_contents( $this->tempFileName, $header ); - - $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) ); - } - - public function provideTestExtractMetaData() { - // phpcs:disable Generic.Files.LineLength - return [ - // Files from https://developers.google.com/speed/webp/gallery2 - [ "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C", - [ 'compression' => 'lossless', 'width' => 400, 'height' => 301 ] ], - [ "\x52\x49\x46\x46\x64\x5B\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x8F\x01\x00\x2C\x01\x00\x41\x4C\x50\x48\xE5\x0E", - [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 400, 'height' => 301 ] ], - [ "\x52\x49\x46\x46\xA8\x72\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x9B\x72\x00\x00\x2F\x81\x81\x62\x10\x8D\x40\x8C\x24\x39\x6E\x73\x73\x38\x01\x96", - [ 'compression' => 'lossless', 'width' => 386, 'height' => 395 ] ], - [ "\x52\x49\x46\x46\xE0\x42\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x81\x01\x00\x8A\x01\x00\x41\x4C\x50\x48\x56\x10", - [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ] ], - [ "\x52\x49\x46\x46\x70\x61\x02\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x63\x61\x02\x00\x2F\x1F\xC3\x95\x10\x8D\xC8\x72\xDB\xC8\x92\x24\xD8\x91\xD9\x91", - [ 'compression' => 'lossless', 'width' => 800, 'height' => 600 ] ], - [ "\x52\x49\x46\x46\x1C\x1D\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x1F\x03\x00\x57\x02\x00\x41\x4C\x50\x48\x25\x8B", - [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 800, 'height' => 600 ] ], - [ "\x52\x49\x46\x46\xFA\xC5\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xEE\xC5\x00\x00\x2F\xA4\x81\x28\x10\x8D\x40\x68\x24\xC9\x91\xA4\xAE\xF3\x97\x75", - [ 'compression' => 'lossless', 'width' => 421, 'height' => 163 ] ], - [ "\x52\x49\x46\x46\xF6\x5D\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\xA4\x01\x00\xA2\x00\x00\x41\x4C\x50\x48\x38\x1A", - [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 421, 'height' => 163 ] ], - [ "\x52\x49\x46\x46\xC4\x96\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xB8\x96\x01\x00\x2F\x2B\xC1\x4A\x10\x11\x87\x6D\xDB\x48\x12\xFC\x60\xB0\x83\x24", - [ 'compression' => 'lossless', 'width' => 300, 'height' => 300 ] ], - [ "\x52\x49\x46\x46\x0A\x11\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x2B\x01\x00\x2B\x01\x00\x41\x4C\x50\x48\x67\x6E", - [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 300, 'height' => 300 ] ], - - // Lossy files from https://developers.google.com/speed/webp/gallery1 - [ "\x52\x49\x46\x46\x68\x76\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\x5C\x76\x00\x00\xD2\xBE\x01\x9D\x01\x2A\x26\x02\x70\x01\x3E\xD5\x4E\x97\x43\xA2", - [ 'compression' => 'lossy', 'width' => 550, 'height' => 368 ] ], - [ "\x52\x49\x46\x46\xB0\xEC\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\xA4\xEC\x00\x00\xB2\x4B\x02\x9D\x01\x2A\x26\x02\x94\x01\x3E\xD1\x50\x96\x46\x26", - [ 'compression' => 'lossy', 'width' => 550, 'height' => 404 ] ], - [ "\x52\x49\x46\x46\x7A\x19\x03\x00\x57\x45\x42\x50\x56\x50\x38\x20\x6E\x19\x03\x00\xB2\xF8\x09\x9D\x01\x2A\x00\x05\xD0\x02\x3E\xAD\x46\x99\x4A\xA5", - [ 'compression' => 'lossy', 'width' => 1280, 'height' => 720 ] ], - [ "\x52\x49\x46\x46\x44\xB3\x02\x00\x57\x45\x42\x50\x56\x50\x38\x20\x38\xB3\x02\x00\x52\x57\x06\x9D\x01\x2A\x00\x04\x04\x03\x3E\xA5\x44\x96\x49\x26", - [ 'compression' => 'lossy', 'width' => 1024, 'height' => 772 ] ], - [ "\x52\x49\x46\x46\x02\x43\x01\x00\x57\x45\x42\x50\x56\x50\x38\x20\xF6\x42\x01\x00\x12\xC0\x05\x9D\x01\x2A\x00\x04\xF0\x02\x3E\x79\x34\x93\x47\xA4", - [ 'compression' => 'lossy', 'width' => 1024, 'height' => 752 ] ], - - // Animated file from https://groups.google.com/a/chromium.org/d/topic/blink-dev/Y8tRC4mdQz8/discussion - [ "\x52\x49\x46\x46\xD0\x0B\x02\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x12\x00\x00\x00\x3F\x01\x00\x3F\x01\x00\x41\x4E", - [ 'compression' => 'unknown', 'animated' => true, 'transparency' => true, 'width' => 320, 'height' => 320 ] ], - - // Error cases - [ '', false ], - [ ' ', false ], - [ 'RIFF ', false ], - [ 'RIFF1234WEBP ', false ], - [ 'RIFF1234WEBPVP8 ', false ], - [ 'RIFF1234WEBPVP8L ', false ], - ]; - // phpcs:enable - } - - /** - * @dataProvider provideTestWithFileExtractMetaData - */ - public function testWithFileExtractMetaData( $filename, $expectedResult ) { - $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $filename ) ); - } - - public function provideTestWithFileExtractMetaData() { - return [ - [ __DIR__ . '/../../data/media/2_webp_ll.webp', - [ - 'compression' => 'lossless', - 'width' => 386, - 'height' => 395 - ] - ], - [ __DIR__ . '/../../data/media/2_webp_a.webp', - [ - 'compression' => 'lossy', - 'animated' => false, - 'transparency' => true, - 'width' => 386, - 'height' => 395 - ] - ], - ]; - } - - /** - * @dataProvider provideTestGetImageSize - */ - public function testGetImageSize( $path, $expectedResult ) { - $handler = new WebPHandler(); - $this->assertEquals( $expectedResult, $handler->getImageSize( null, $path ) ); - } - - public function provideTestGetImageSize() { - return [ - // Public domain files from https://developers.google.com/speed/webp/gallery2 - [ __DIR__ . '/../../data/media/2_webp_a.webp', [ 386, 395 ] ], - [ __DIR__ . '/../../data/media/2_webp_ll.webp', [ 386, 395 ] ], - [ __DIR__ . '/../../data/media/webp_animated.webp', [ 300, 225 ] ], - - // Error cases - [ __FILE__, false ], - ]; - } - - /** - * Tests the WebP MIME detection. This should really be a separate test, but sticking it - * here for now. - * - * @dataProvider provideTestGetMimeType - */ - public function testGuessMimeType( $path ) { - $mime = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); - $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) ); - } - - public function provideTestGetMimeType() { - return [ - // Public domain files from https://developers.google.com/speed/webp/gallery2 - [ __DIR__ . '/../../data/media/2_webp_a.webp' ], - [ __DIR__ . '/../../data/media/2_webp_ll.webp' ], - [ __DIR__ . '/../../data/media/webp_animated.webp' ], - ]; - } -} - -/* Python code to extract a header and convert to PHP format: - * print '"%s"' % ''.implode( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) ) - */ diff --git a/tests/phpunit/includes/media/XCFHandlerTest.php b/tests/phpunit/includes/media/XCFHandlerTest.php new file mode 100644 index 0000000000..b75335d6c0 --- /dev/null +++ b/tests/phpunit/includes/media/XCFHandlerTest.php @@ -0,0 +1,83 @@ +handler = new XCFHandler(); + } + + /** + * @param string $filename + * @param int $expectedWidth Width + * @param int $expectedHeight Height + * @dataProvider provideGetImageSize + * @covers XCFHandler::getImageSize + */ + public function testGetImageSize( $filename, $expectedWidth, $expectedHeight ) { + $file = $this->dataFile( $filename, 'image/x-xcf' ); + $actual = $this->handler->getImageSize( $file, $file->getLocalRefPath() ); + $this->assertEquals( $expectedWidth, $actual[0] ); + $this->assertEquals( $expectedHeight, $actual[1] ); + } + + public static function provideGetImageSize() { + return [ + [ '80x60-2layers.xcf', 80, 60 ], + [ '80x60-RGB.xcf', 80, 60 ], + [ '80x60-Greyscale.xcf', 80, 60 ], + ]; + } + + /** + * @param string $metadata Serialized metadata + * @param int $expected One of the class constants of XCFHandler + * @dataProvider provideIsMetadataValid + * @covers XCFHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + return [ + [ '', XCFHandler::METADATA_BAD ], + [ serialize( [ 'error' => true ] ), XCFHandler::METADATA_GOOD ], + [ false, XCFHandler::METADATA_BAD ], + [ serialize( [ 'colorType' => 'greyscale-alpha' ] ), XCFHandler::METADATA_GOOD ], + ]; + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetMetadata + * @covers XCFHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetMetadata() { + return [ + [ '80x60-2layers.xcf', + 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' + ], + [ '80x60-RGB.xcf', + 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' + ], + [ '80x60-Greyscale.xcf', + 'a:1:{s:9:"colorType";s:15:"greyscale-alpha";}' + ], + ]; + } +} diff --git a/tests/phpunit/includes/media/XCFTest.php b/tests/phpunit/includes/media/XCFTest.php deleted file mode 100644 index b75335d6c0..0000000000 --- a/tests/phpunit/includes/media/XCFTest.php +++ /dev/null @@ -1,83 +0,0 @@ -handler = new XCFHandler(); - } - - /** - * @param string $filename - * @param int $expectedWidth Width - * @param int $expectedHeight Height - * @dataProvider provideGetImageSize - * @covers XCFHandler::getImageSize - */ - public function testGetImageSize( $filename, $expectedWidth, $expectedHeight ) { - $file = $this->dataFile( $filename, 'image/x-xcf' ); - $actual = $this->handler->getImageSize( $file, $file->getLocalRefPath() ); - $this->assertEquals( $expectedWidth, $actual[0] ); - $this->assertEquals( $expectedHeight, $actual[1] ); - } - - public static function provideGetImageSize() { - return [ - [ '80x60-2layers.xcf', 80, 60 ], - [ '80x60-RGB.xcf', 80, 60 ], - [ '80x60-Greyscale.xcf', 80, 60 ], - ]; - } - - /** - * @param string $metadata Serialized metadata - * @param int $expected One of the class constants of XCFHandler - * @dataProvider provideIsMetadataValid - * @covers XCFHandler::isMetadataValid - */ - public function testIsMetadataValid( $metadata, $expected ) { - $actual = $this->handler->isMetadataValid( null, $metadata ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideIsMetadataValid() { - return [ - [ '', XCFHandler::METADATA_BAD ], - [ serialize( [ 'error' => true ] ), XCFHandler::METADATA_GOOD ], - [ false, XCFHandler::METADATA_BAD ], - [ serialize( [ 'colorType' => 'greyscale-alpha' ] ), XCFHandler::METADATA_GOOD ], - ]; - } - - /** - * @param string $filename - * @param string $expected Serialized array - * @dataProvider provideGetMetadata - * @covers XCFHandler::getMetadata - */ - public function testGetMetadata( $filename, $expected ) { - $file = $this->dataFile( $filename, 'image/png' ); - $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); - $this->assertEquals( $expected, $actual ); - } - - public static function provideGetMetadata() { - return [ - [ '80x60-2layers.xcf', - 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' - ], - [ '80x60-RGB.xcf', - 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' - ], - [ '80x60-Greyscale.xcf', - 'a:1:{s:9:"colorType";s:15:"greyscale-alpha";}' - ], - ]; - } -} diff --git a/tests/phpunit/includes/page/ArticleViewTest.php b/tests/phpunit/includes/page/ArticleViewTest.php index 466e209a32..524fbdccef 100644 --- a/tests/phpunit/includes/page/ArticleViewTest.php +++ b/tests/phpunit/includes/page/ArticleViewTest.php @@ -190,8 +190,7 @@ class ArticleViewTest extends MediaWikiTestCase { ->willReturn( new ParserOutput( 'Structured Output' ) ); $content->method( 'getModel' ) ->willReturn( 'NotText' ); - $content->method( 'getNativeData' ) - ->willReturn( [ (object)[ 'x' => 'stuff' ] ] ); + $content->expects( $this->never() )->method( 'getNativeData' ); $content->method( 'copy' ) ->willReturn( $content ); @@ -447,7 +446,7 @@ class ArticleViewTest extends MediaWikiTestCase { 'ArticleContentViewCustom', function ( Content $content, Title $title, OutputPage $output ) use ( $page ) { $this->assertSame( $page->getTitle(), $title, '$title' ); - $this->assertSame( 'Test A', $content->getNativeData(), '$content' ); + $this->assertSame( 'Test A', $content->getText(), '$content' ); $output->addHTML( 'Hook Text' ); return false; @@ -483,9 +482,8 @@ class ArticleViewTest extends MediaWikiTestCase { 'ArticleRevisionViewCustom', function ( RevisionRecord $rev, Title $title, $oldid, OutputPage $output ) use ( $page ) { $content = $rev->getContent( SlotRecord::MAIN ); - $this->assertSame( $page->getTitle(), $title, '$title' ); - $this->assertSame( 'Test A', $content->getNativeData(), '$content' ); + $this->assertSame( 'Test A', $content->getText(), '$content' ); $output->addHTML( 'Hook Text' ); return false; @@ -517,7 +515,7 @@ class ArticleViewTest extends MediaWikiTestCase { 'ArticleAfterFetchContentObject', function ( Article &$articlePage, Content &$content ) use ( $page, $article ) { $this->assertSame( $article, $articlePage, '$articlePage' ); - $this->assertSame( 'Test A', $content->getNativeData(), '$content' ); + $this->assertSame( 'Test A', $content->getText(), '$content' ); $content = new WikitextContent( 'Hook Text' ); } diff --git a/tests/phpunit/includes/page/ImagePageTest.php b/tests/phpunit/includes/page/ImagePageTest.php index 8e49bf9865..7e43ce4e65 100644 --- a/tests/phpunit/includes/page/ImagePageTest.php +++ b/tests/phpunit/includes/page/ImagePageTest.php @@ -1,7 +1,10 @@ setMwGlobals( 'wgImageLimits', [ [ 320, 240 ], [ 640, 480 ], @@ -12,7 +15,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { parent::setUp(); } - function getImagePage( $filename ) { + public function getImagePage( $filename ) { $title = Title::makeTitleSafe( NS_FILE, $filename ); $file = $this->dataFile( $filename ); $iPage = new ImagePage( $title ); @@ -26,7 +29,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { * @param array $dim Array [maxWidth, maxHeight, width, height] * @param array $expected Array [width, height] The width and height we expect to display at */ - function testGetDisplayWidthHeight( $dim, $expected ) { + public function testGetDisplayWidthHeight( $dim, $expected ) { $iPage = $this->getImagePage( 'animated.gif' ); $reflection = new ReflectionClass( $iPage ); $reflMethod = $reflection->getMethod( 'getDisplayWidthHeight' ); @@ -36,7 +39,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { $this->assertEquals( $actual, $expected ); } - function providerGetDisplayWidthHeight() { + public function providerGetDisplayWidthHeight() { return [ [ [ 1024.0, 768.0, 600.0, 600.0 ], @@ -71,7 +74,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { * @param string $filename * @param int $expectedNumberThumbs How many thumbnails to show */ - function testGetThumbSizes( $filename, $expectedNumberThumbs ) { + public function testGetThumbSizes( $filename, $expectedNumberThumbs ) { $iPage = $this->getImagePage( $filename ); $reflection = new ReflectionClass( $iPage ); $reflMethod = $reflection->getMethod( 'getThumbSizes' ); @@ -81,7 +84,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { $this->assertEquals( count( $actual ), $expectedNumberThumbs ); } - function providerGetThumbSizes() { + public function providerGetThumbSizes() { return [ [ 'animated.gif', 2 ], [ 'Toll_Texas_1.svg', 1 ], @@ -89,4 +92,41 @@ class ImagePageTest extends MediaWikiMediaTestCase { [ 'jpeg-comment-binary.jpg', 2 ], ]; } + + /** + * @covers ImagePage::getLanguageForRendering() + * @dataProvider provideGetLanguageForRendering + * + * @param string|null $expected Expected language code + * @param string $wikiLangCode Wiki language code + * @param string|null $lang lang=... URL parameter + */ + public function testGetLanguageForRendering( $expected = null, $wikiLangCode, $lang = null ) { + $params = []; + if ( $lang !== null ) { + $params['lang'] = $lang; + } + $request = new FauxRequest( $params ); + $this->setMwGlobals( 'wgLanguageCode', $wikiLangCode ); + + $page = $this->getImagePage( 'translated.svg' ); + $page = TestingAccessWrapper::newFromObject( $page ); + + /** @var ImagePage $page */ + $result = $page->getLanguageForRendering( $request, $page->getDisplayedFile() ); + $this->assertEquals( $expected, $result ); + } + + public function provideGetLanguageForRendering() { + return [ + [ 'ru', 'ru' ], + [ 'ru', 'ru', 'ru' ], + [ null, 'en' ], + [ null, 'fr' ], + [ null, 'en', 'en' ], + [ null, 'fr', 'fr' ], + [ null, 'ru', 'en' ], + [ 'de', 'ru', 'de' ], + ]; + } } diff --git a/tests/phpunit/includes/page/PageArchiveTestBase.php b/tests/phpunit/includes/page/PageArchiveTestBase.php index 26b6b5234d..06c0456ac7 100644 --- a/tests/phpunit/includes/page/PageArchiveTestBase.php +++ b/tests/phpunit/includes/page/PageArchiveTestBase.php @@ -82,7 +82,6 @@ abstract class PageArchiveTestBase extends MediaWikiTestCase { $this->tablesUsed += $this->getMcrTablesToReset(); - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_NEW ); $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD ); $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); $this->setMwGlobals( diff --git a/tests/phpunit/includes/page/WikiPageDbTestBase.php b/tests/phpunit/includes/page/WikiPageDbTestBase.php index 298dc52a3c..ac5fef9389 100644 --- a/tests/phpunit/includes/page/WikiPageDbTestBase.php +++ b/tests/phpunit/includes/page/WikiPageDbTestBase.php @@ -737,7 +737,7 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { $rev = $page->getRevision(); $this->assertEquals( $page->getLatest(), $rev->getId() ); - $this->assertEquals( "some text", $rev->getContent()->getNativeData() ); + $this->assertEquals( "some text", $rev->getContent()->getText() ); } /** @@ -753,7 +753,7 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); $content = $page->getContent(); - $this->assertEquals( "some text", $content->getNativeData() ); + $this->assertEquals( "some text", $content->getText() ); } /** @@ -851,7 +851,7 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { # now, test the actual redirect $t = $page->getRedirectTarget(); - $this->assertEquals( $target, is_null( $t ) ? null : $t->getFullText() ); + $this->assertEquals( $target, $t ? $t->getFullText() : null ); } /** @@ -1109,9 +1109,10 @@ more stuff $page = $this->createPage( $title, $text, $model ); $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() ); + /** @var TextContent $c */ $c = $page->replaceSectionContent( $section, $content, $sectionTitle ); - $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) ); + $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null ); } /** @@ -1125,9 +1126,10 @@ more stuff $baseRevId = $page->getLatest(); $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() ); + /** @var TextContent $c */ $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId ); - $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) ); + $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null ); } /** @@ -1242,7 +1244,7 @@ more stuff $page = new WikiPage( $page->getTitle() ); $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); - $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() ); + $this->assertEquals( "one\n\ntwo", $page->getContent()->getText() ); $rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange( $page->getRevision()->getRevisionRecord() @@ -1332,7 +1334,7 @@ more stuff $page = new WikiPage( $page->getTitle() ); $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); - $this->assertEquals( "one", $page->getContent()->getNativeData() ); + $this->assertEquals( "one", $page->getContent()->getText() ); } /** @@ -1560,89 +1562,6 @@ more stuff $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) ); } - /** - * @dataProvider provideCommentMigrationOnDeletion - * - * @param int $writeStage - * @param int $readStage - */ - public function testCommentMigrationOnDeletion( $writeStage, $readStage ) { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $writeStage ); - $this->overrideMwServices(); - - $dbr = wfGetDB( DB_REPLICA ); - - $page = $this->createPage( - __METHOD__, - "foo", - CONTENT_MODEL_WIKITEXT - ); - $revid = $page->getLatest(); - if ( $writeStage > MIGRATION_OLD ) { - $comment_id = $dbr->selectField( - 'revision_comment_temp', - 'revcomment_comment_id', - [ 'revcomment_rev' => $revid ], - __METHOD__ - ); - } - - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $readStage ); - $this->overrideMwServices(); - - $page->doDeleteArticle( "testing deletion" ); - - if ( $readStage > MIGRATION_OLD ) { - // Didn't leave behind any 'revision_comment_temp' rows - $n = $dbr->selectField( - 'revision_comment_temp', 'COUNT(*)', [ 'revcomment_rev' => $revid ], __METHOD__ - ); - $this->assertEquals( 0, $n, 'no entry in revision_comment_temp after deletion' ); - - // Copied or upgraded the comment_id, as applicable - $ar_comment_id = $dbr->selectField( - 'archive', - 'ar_comment_id', - [ 'ar_rev_id' => $revid ], - __METHOD__ - ); - if ( $writeStage > MIGRATION_OLD ) { - $this->assertSame( $comment_id, $ar_comment_id ); - } else { - $this->assertNotEquals( 0, $ar_comment_id ); - } - } - - // Copied rev_comment, if applicable - if ( $readStage <= MIGRATION_WRITE_BOTH && $writeStage <= MIGRATION_WRITE_BOTH ) { - $ar_comment = $dbr->selectField( - 'archive', - 'ar_comment', - [ 'ar_rev_id' => $revid ], - __METHOD__ - ); - $this->assertSame( 'testing', $ar_comment ); - } - } - - public function provideCommentMigrationOnDeletion() { - return [ - [ MIGRATION_OLD, MIGRATION_OLD ], - [ MIGRATION_OLD, MIGRATION_WRITE_BOTH ], - [ MIGRATION_OLD, MIGRATION_WRITE_NEW ], - [ MIGRATION_WRITE_BOTH, MIGRATION_OLD ], - [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_BOTH ], - [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ], - [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], - [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_BOTH ], - [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_NEW ], - [ MIGRATION_WRITE_NEW, MIGRATION_NEW ], - [ MIGRATION_NEW, MIGRATION_WRITE_BOTH ], - [ MIGRATION_NEW, MIGRATION_WRITE_NEW ], - [ MIGRATION_NEW, MIGRATION_NEW ], - ]; - } - /** * @covers WikiPage::updateCategoryCounts */ @@ -1900,8 +1819,8 @@ more stuff $fetchedPage = WikiPage::newFromID( $createdPage->getId() ); $this->assertSame( $createdPage->getId(), $fetchedPage->getId() ); $this->assertEquals( - $createdPage->getContent()->getNativeData(), - $fetchedPage->getContent()->getNativeData() + $createdPage->getContent()->getText(), + $fetchedPage->getContent()->getText() ); } diff --git a/tests/phpunit/includes/parser/ParserOutputTest.php b/tests/phpunit/includes/parser/ParserOutputTest.php index 390ea415a0..af2b9b7a1f 100644 --- a/tests/phpunit/includes/parser/ParserOutputTest.php +++ b/tests/phpunit/includes/parser/ParserOutputTest.php @@ -397,7 +397,6 @@ EOF $a->addHeadItem( '' ); $a->addHeadItem( '', 'bar' ); $a->addModules( 'test-module-a' ); - $a->addModuleScripts( 'test-module-script-a' ); $a->addModuleStyles( 'test-module-styles-a' ); $b->addJsConfigVars( 'test-config-var-a', 'a' ); @@ -406,7 +405,6 @@ EOF $b->addHeadItem( '' ); $b->addHeadItem( '', 'bar' ); $b->addModules( 'test-module-b' ); - $b->addModuleScripts( 'test-module-script-b' ); $b->addModuleStyles( 'test-module-styles-b' ); $b->addJsConfigVars( 'test-config-var-b', 'b' ); $b->addJsConfigVars( 'test-config-var-a', 'X' ); @@ -421,10 +419,6 @@ EOF 'test-module-a', 'test-module-b', ], - 'getModuleScripts' => [ - 'test-module-script-a', - 'test-module-script-b', - ], 'getModuleStyles' => [ 'test-module-styles-a', 'test-module-styles-b', @@ -849,6 +843,11 @@ EOF $this->assertFieldValues( $a, $expected ); } + /** + * @covers ParserOutput::mergeInternalMetaDataFrom + * @covers ParserOutput::getTimes + * @covers ParserOutput::resetParseStartTime + */ public function testMergeInternalMetaDataFrom_parseStartTime() { /** @var object $a */ $a = new ParserOutput(); diff --git a/tests/phpunit/includes/parser/SanitizerTest.php b/tests/phpunit/includes/parser/SanitizerTest.php index ad8aa1e7eb..1f6f4e873b 100644 --- a/tests/phpunit/includes/parser/SanitizerTest.php +++ b/tests/phpunit/includes/parser/SanitizerTest.php @@ -527,6 +527,7 @@ class SanitizerTest extends MediaWikiTestCase { ], [ '123', '123' ], [ '123', '123' ], + [ '12', '1 2' ], ]; } diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php index be5125c7e3..898ef2d163 100644 --- a/tests/phpunit/includes/parser/TidyTest.php +++ b/tests/phpunit/includes/parser/TidyTest.php @@ -2,6 +2,7 @@ /** * @group Parser + * @covers MWTidy */ class TidyTest extends MediaWikiTestCase { diff --git a/tests/phpunit/includes/password/Argon2PasswordTest.php b/tests/phpunit/includes/password/Argon2PasswordTest.php new file mode 100644 index 0000000000..b518040e87 --- /dev/null +++ b/tests/phpunit/includes/password/Argon2PasswordTest.php @@ -0,0 +1,105 @@ +markTestSkipped( 'Argon2 support not found' ); + } + } + + /** + * Return an array of configs to be used for this class's password type. + * + * @return array[] + */ + protected function getTypeConfigs() { + return [ + 'argon2' => [ + 'class' => Argon2Password::class, + 'algo' => 'argon2i', + 'memory_cost' => 1024, + 'time_cost' => 2, + 'threads' => 2, + ] + ]; + } + + /** + * @return array + */ + public static function providePasswordTests() { + $result = [ + [ + true, + ':argon2:$argon2i$v=19$m=1024,t=2,p=2$RHpGTXJPeFlSV2NDTEswNA$VeW7rumZY4pL8XO4KeQkKD43r5uX3eazVJRtrFN7lNc', + 'password', + ], + [ + true, + ':argon2:$argon2i$v=19$m=2048,t=5,p=3$MHFKSnh6WWZEWkpKa09SUQ$vU92h/8hkByL5VKW1P9amCj054pZILGKznAvKWAivZE', + 'password', + ], + [ + true, + ':argon2:$argon2i$v=19$m=1024,t=2,p=2$bFJ4TzM5RWh2T0VmeFhDTA$AHFUFZRh69aZYBqyxn6tpujpEcf2JP8wgRCPU3nw3W4', + "pass\x00word", + ], + [ + false, + ':argon2:$argon2i$v=19$m=1024,t=2,p=2$UGZqTWJRUkI1alVNTGRUbA$RcASw9XUWjCDO9WNnuVkGkEylURUW/CcNwSffdFwN74', + 'password', + ] + ]; + + if ( defined( 'PASSWORD_ARGON2ID' ) ) { + // @todo: Argon2id cases + $result = array_merge( $result, [] ); + } + + return $result; + } + + /** + * @dataProvider provideNeedsUpdate + */ + public function testNeedsUpdate( $updateExpected, $hash ) { + $password = $this->passwordFactory->newFromCiphertext( $hash ); + $this->assertSame( $updateExpected, $password->needsUpdate() ); + } + + public function provideNeedsUpdate() { + return [ + [ false, ':argon2:$argon2i$v=19$m=1024,t=2,p=2$bFJ4TzM5RWh2T0VmeFhDTA$AHFUFZRh69aZYBqyxn6tpujpEcf2JP8wgRCPU3nw3W4' ], + [ false, ':argon2:$argon2i$v=19$m=1024,t=2,p=2$' ], + [ true, ':argon2:$argon2i$v=19$m=666,t=2,p=2$' ], + [ true, ':argon2:$argon2i$v=19$m=1024,t=666,p=2$' ], + [ true, ':argon2:$argon2i$v=19$m=1024,t=2,p=666$' ], + ]; + } + + public function testPartialConfig() { + $factory = new PasswordFactory(); + $factory->register( 'argon2', [ + 'class' => Argon2Password::class, + 'algo' => 'argon2i', + ] ); + + $partialPassword = $factory->newFromType( 'argon2' ); + $partialPassword->crypt( 'password' ); + $fullPassword = $this->passwordFactory->newFromCiphertext( $partialPassword->toString() ); + + $this->assertFalse( $fullPassword->needsUpdate(), + 'Options not set for a password should fall back to defaults' + ); + } +} diff --git a/tests/phpunit/includes/password/EncryptedPasswordTest.php b/tests/phpunit/includes/password/EncryptedPasswordTest.php index c5d397fbc7..7384310aa2 100644 --- a/tests/phpunit/includes/password/EncryptedPasswordTest.php +++ b/tests/phpunit/includes/password/EncryptedPasswordTest.php @@ -78,6 +78,7 @@ class EncryptedPasswordTest extends PasswordTestCase { $this->assertRegExp( '/^:both:aes-256-cbc:1:/', $serialized ); $fromNewHash = $this->passwordFactory->newFromCiphertext( $serialized ); $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromNewHash ); - $this->assertTrue( $fromHash->equals( $fromPlaintext ) ); + $this->assertTrue( $fromPlaintext->verify( 'password' ) ); + $this->assertTrue( $fromHash->verify( 'password' ) ); } } diff --git a/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php index 6a965a0387..0f848ab1ee 100644 --- a/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php +++ b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php @@ -58,6 +58,6 @@ class LayeredParameterizedPasswordTest extends PasswordTestCase { $totalPassword = $this->passwordFactory->newFromType( 'testLargeLayeredTop' ); $totalPassword->partialCrypt( $partialPassword ); - $this->assertTrue( $totalPassword->equals( 'testPassword123' ) ); + $this->assertTrue( $totalPassword->verify( 'testPassword123' ) ); } } diff --git a/tests/phpunit/includes/password/PasswordPolicyChecksTest.php b/tests/phpunit/includes/password/PasswordPolicyChecksTest.php index 9f9824f8b7..457030fe5c 100644 --- a/tests/phpunit/includes/password/PasswordPolicyChecksTest.php +++ b/tests/phpunit/includes/password/PasswordPolicyChecksTest.php @@ -181,6 +181,7 @@ class PasswordPolicyChecksTest extends MediaWikiTestCase { /** * Verify that all password policy description messages actually exist. * Messages used on Special:PasswordPolicies + * @coversNothing */ public function testPasswordPolicyDescriptionsExist() { global $wgPasswordPolicy; diff --git a/tests/phpunit/includes/password/PasswordTest.php b/tests/phpunit/includes/password/PasswordTest.php index 65c9199310..61a5147277 100644 --- a/tests/phpunit/includes/password/PasswordTest.php +++ b/tests/phpunit/includes/password/PasswordTest.php @@ -24,14 +24,6 @@ * @covers InvalidPassword */ class PasswordTest extends MediaWikiTestCase { - public function testInvalidUnequalInvalid() { - $passwordFactory = new PasswordFactory(); - $invalid1 = $passwordFactory->newFromCiphertext( null ); - $invalid2 = $passwordFactory->newFromCiphertext( null ); - - $this->assertFalse( $invalid1->equals( $invalid2 ) ); - } - public function testInvalidPlaintext() { $passwordFactory = new PasswordFactory(); $invalid = $passwordFactory->newFromPlaintext( null ); diff --git a/tests/phpunit/includes/password/PasswordTestCase.php b/tests/phpunit/includes/password/PasswordTestCase.php index 7afdd0abb8..d1e257895e 100644 --- a/tests/phpunit/includes/password/PasswordTestCase.php +++ b/tests/phpunit/includes/password/PasswordTestCase.php @@ -60,9 +60,8 @@ abstract class PasswordTestCase extends MediaWikiTestCase { * @dataProvider providePasswordTests */ public function testHashing( $shouldMatch, $hash, $password ) { - $hash = $this->passwordFactory->newFromCiphertext( $hash ); - $password = $this->passwordFactory->newFromPlaintext( $password, $hash ); - $this->assertSame( $shouldMatch, $hash->equals( $password ) ); + $passwordObj = $this->passwordFactory->newFromCiphertext( $hash ); + $this->assertSame( $shouldMatch, $passwordObj->verify( $password ) ); } /** @@ -72,7 +71,7 @@ abstract class PasswordTestCase extends MediaWikiTestCase { $hashObj = $this->passwordFactory->newFromCiphertext( $hash ); $serialized = $hashObj->toString(); $unserialized = $this->passwordFactory->newFromCiphertext( $serialized ); - $this->assertTrue( $hashObj->equals( $unserialized ) ); + $this->assertEquals( $hashObj->toString(), $unserialized->toString() ); } /** @@ -85,6 +84,7 @@ abstract class PasswordTestCase extends MediaWikiTestCase { $this->assertFalse( $invalid->equals( $normal ) ); $this->assertFalse( $normal->equals( $invalid ) ); + $this->assertFalse( $invalid->verify( $hash ) ); } protected function getValidTypes() { @@ -106,6 +106,13 @@ abstract class PasswordTestCase extends MediaWikiTestCase { $fromType = $this->passwordFactory->newFromType( $type ); $fromType->crypt( 'password' ); $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromType ); - $this->assertTrue( $fromType->equals( $fromPlaintext ) ); + $this->assertTrue( $fromType->verify( 'password' ) ); + $this->assertTrue( $fromPlaintext->verify( 'password' ) ); + $this->assertFalse( $fromType->verify( 'different password' ) ); + $this->assertFalse( $fromPlaintext->verify( 'different password' ) ); + $this->assertEquals( get_class( $fromType ), + get_class( $fromPlaintext ), + 'newFromPlaintext() should produce instance of the same class as newFromType()' + ); } } diff --git a/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php b/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php index 7a47f4c7d6..a58cf2a791 100644 --- a/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php +++ b/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php @@ -1,6 +1,5 @@ true, ], 'sysop' => [ - 'MinimalPasswordLength' => 8, + 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true ], 'MinimumPasswordLengthToLogin' => 1, 'PasswordCannotMatchUsername' => true, ], + 'bureaucrat' => [ + 'MinimalPasswordLength' => [ + 'value' => 6, + 'suggestChangeOnLogin' => false, + 'forceChange' => true, + ], + 'PasswordCannotMatchUsername' => true, + ], 'default' => [ 'MinimalPasswordLength' => 4, 'MinimumPasswordLengthToLogin' => 1, @@ -67,7 +75,7 @@ class UserPasswordPolicyTest extends MediaWikiTestCase { $user = $this->getTestUser( [ 'sysop' ] )->getUser(); $this->assertArrayEquals( [ - 'MinimalPasswordLength' => 8, + 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true ], 'MinimumPasswordLengthToLogin' => 1, 'PasswordCannotMatchUsername' => true, 'PasswordCannotMatchBlacklist' => true, @@ -79,7 +87,11 @@ class UserPasswordPolicyTest extends MediaWikiTestCase { $user = $this->getTestUser( [ 'sysop', 'checkuser' ] )->getUser(); $this->assertArrayEquals( [ - 'MinimalPasswordLength' => [ 'value' => 10, 'forceChange' => true ], + 'MinimalPasswordLength' => [ + 'value' => 10, + 'forceChange' => true, + 'suggestChangeOnLogin' => true + ], 'MinimumPasswordLengthToLogin' => 6, 'PasswordCannotMatchUsername' => true, 'PasswordCannotMatchBlacklist' => true, @@ -92,13 +104,17 @@ class UserPasswordPolicyTest extends MediaWikiTestCase { public function testGetPoliciesForGroups() { $effective = UserPasswordPolicy::getPoliciesForGroups( $this->policies, - [ 'user', 'checkuser' ], + [ 'user', 'checkuser', 'sysop' ], $this->policies['default'] ); $this->assertArrayEquals( [ - 'MinimalPasswordLength' => [ 'value' => 10, 'forceChange' => true ], + 'MinimalPasswordLength' => [ + 'value' => 10, + 'forceChange' => true, + 'suggestChangeOnLogin' => true + ], 'MinimumPasswordLengthToLogin' => 6, 'PasswordCannotMatchUsername' => true, 'PasswordCannotMatchBlacklist' => true, @@ -125,12 +141,16 @@ class UserPasswordPolicyTest extends MediaWikiTestCase { $success = Status::newGood( [] ); $warning = Status::newGood( [] ); $forceChange = Status::newGood( [ 'forceChange' => true ] ); + $suggestChangeOnLogin = Status::newGood( [ 'suggestChangeOnLogin' => true ] ); $fatal = Status::newGood( [] ); + // the message does not matter, we only test for state and value $warning->warning( 'invalid-password' ); $forceChange->warning( 'invalid-password' ); + $suggestChangeOnLogin->warning( 'invalid-password' ); $warning->warning( 'invalid-password' ); $fatal->fatal( 'invalid-password' ); + return [ 'No groups, default policy, password too short to login' => [ [], @@ -147,16 +167,21 @@ class UserPasswordPolicyTest extends MediaWikiTestCase { 'abcdabcdabcd', $success, ], - 'Sysop with short password' => [ + 'Sysop with short password and suggestChangeOnLogin set to true' => [ [ 'sysop' ], 'abcd', - $warning, + $suggestChangeOnLogin, ], 'Checkuser with short password' => [ - [ 'sysop', 'checkuser' ], + [ 'checkuser' ], 'abcdabcd', $forceChange, ], + 'Bureaucrat bad password with forceChange true, suggestChangeOnLogin false' => [ + [ 'bureaucrat' ], + 'short', + $forceChange, + ], 'Checkuser with too short password to login' => [ [ 'sysop', 'checkuser' ], 'abcd', @@ -281,6 +306,29 @@ class UserPasswordPolicyTest extends MediaWikiTestCase { ], ], // max ], + 'complex value in both p1 and p2 #2' => [ + [ + 'MinimalPasswordLength' => [ + 'value' => 8, + 'foo' => 1, + 'baz' => false, + ], + ], // p1 + [ + 'MinimalPasswordLength' => [ + 'value' => 2, + 'bar' => true + ], + ], // p2 + [ + 'MinimalPasswordLength' => [ + 'value' => 8, + 'foo' => 1, + 'bar' => true, + 'baz' => false, + ], + ], // max + ], ]; } diff --git a/tests/phpunit/includes/poolcounter/PoolCounterTest.php b/tests/phpunit/includes/poolcounter/PoolCounterTest.php index f7f2013cb4..19baf5a82f 100644 --- a/tests/phpunit/includes/poolcounter/PoolCounterTest.php +++ b/tests/phpunit/includes/poolcounter/PoolCounterTest.php @@ -1,14 +1,5 @@ assertEquals( $cssClass, $prefs['emailauthentication']['cssclass'] ); } + /** + * @covers MediaWiki\Preferences\DefaultPreferencesFactory::renderingPreferences() + */ + public function testShowRollbackConfIsHiddenForUsersWithoutRollbackRights() { + $userMock = $this->getMockBuilder( User::class ) + ->disableOriginalConstructor() + ->getMock(); + $userMock->method( 'isAllowed' ) + ->willReturn( false ); + $userMock->method( 'getEffectiveGroups' ) + ->willReturn( [] ); + $userMock->method( 'getGroupMemberships' ) + ->willReturn( [] ); + $userMock->method( 'getOptions' ) + ->willReturn( [ 'test' => 'yes' ] ); + + $prefs = $this->getPreferencesFactory()->getFormDescriptor( $userMock, $this->context ); + $this->assertArrayNotHasKey( 'showrollbackconfirmation', $prefs ); + } + + /** + * @covers MediaWiki\Preferences\DefaultPreferencesFactory::renderingPreferences() + */ + public function testShowRollbackConfIsShownForUsersWithRollbackRights() { + $userMock = $this->getMockBuilder( User::class ) + ->disableOriginalConstructor() + ->getMock(); + $userMock->method( 'isAllowed' ) + ->willReturn( true ); + $userMock->method( 'getEffectiveGroups' ) + ->willReturn( [] ); + $userMock->method( 'getGroupMemberships' ) + ->willReturn( [] ); + $userMock->method( 'getOptions' ) + ->willReturn( [ 'test' => 'yes' ] ); + + $prefs = $this->getPreferencesFactory()->getFormDescriptor( $userMock, $this->context ); + $this->assertArrayHasKey( 'showrollbackconfirmation', $prefs ); + $this->assertEquals( + 'rendering/advancedrendering', + $prefs['showrollbackconfirmation']['section'] + ); + } + public function emailAuthenticationProvider() { $userNoEmail = new User; $userEmailUnauthed = new User; diff --git a/tests/phpunit/includes/registration/ExtensionProcessorTest.php b/tests/phpunit/includes/registration/ExtensionProcessorTest.php index 71a3a4fa80..d5a2b3a5a7 100644 --- a/tests/phpunit/includes/registration/ExtensionProcessorTest.php +++ b/tests/phpunit/includes/registration/ExtensionProcessorTest.php @@ -357,13 +357,20 @@ class ExtensionProcessorTest extends MediaWikiTestCase { /** * @dataProvider provideExtractResourceLoaderModules */ - public function testExtractResourceLoaderModules( $input, $expected ) { + public function testExtractResourceLoaderModules( + $input, + array $expectedGlobals, + array $expectedAttribs = [] + ) { $processor = new ExtensionProcessor(); $processor->extractInfo( $this->dir, $input + self::$default, 1 ); $out = $processor->getExtractedInfo(); - foreach ( $expected as $key => $value ) { + foreach ( $expectedGlobals as $key => $value ) { $this->assertEquals( $value, $out['globals'][$key] ); } + foreach ( $expectedAttribs as $key => $value ) { + $this->assertEquals( $value, $out['attributes'][$key] ); + } } public static function provideExtractResourceLoaderModules() { @@ -503,6 +510,27 @@ class ExtensionProcessorTest extends MediaWikiTestCase { ], ], ], + 'QUnit test module' => [ + // Input + [ + 'QUnitTestModule' => [ + 'localBasePath' => '', + 'remoteExtPath' => 'Foo', + 'scripts' => 'bar.js', + ], + ], + // Expected + [], + [ + 'QUnitTestModules' => [ + 'test.FooBar' => [ + 'localBasePath' => $dir, + 'remoteExtPath' => 'Foo', + 'scripts' => 'bar.js', + ], + ], + ], + ], ]; } diff --git a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php index 97ffd9413b..c210061191 100644 --- a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php +++ b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php @@ -8,10 +8,10 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase { use MediaWikiCoversValidator; - protected static function getContext() { + protected static function makeContext() { $request = new FauxRequest( [ - 'lang' => 'zh', - 'modules' => 'test.context', + 'lang' => 'qqx', + 'modules' => 'test.default', 'only' => 'scripts', 'skin' => 'fallback', 'target' => 'test', @@ -19,123 +19,114 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase { return new ResourceLoaderContext( new ResourceLoader(), $request ); } - public function testGetInherited() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); - - // Request parameters - $this->assertEquals( $derived->getDebug(), false ); - $this->assertEquals( $derived->getLanguage(), 'zh' ); - $this->assertEquals( $derived->getModules(), [ 'test.context' ] ); - $this->assertEquals( $derived->getOnly(), 'scripts' ); - $this->assertEquals( $derived->getSkin(), 'fallback' ); - $this->assertEquals( $derived->getUser(), null ); - - // Misc - $this->assertEquals( $derived->getDirection(), 'ltr' ); - $this->assertEquals( $derived->getHash(), 'zh|fallback|||scripts|||||' ); - } - - public function testModules() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeModules() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getModules(), [ 'test.default' ], 'inherit from parent' ); $derived->setModules( [ 'test.override' ] ); - $this->assertEquals( $derived->getModules(), [ 'test.override' ] ); - } - - public function testLanguage() { - $context = self::getContext(); - $derived = new DerivativeResourceLoaderContext( $context ); - - $derived->setLanguage( 'nl' ); - $this->assertEquals( $derived->getLanguage(), 'nl' ); + $this->assertSame( $derived->getModules(), [ 'test.override' ] ); } - public function testDirection() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeLanguageAndDirection() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getLanguage(), 'qqx', 'inherit from parent' ); $derived->setLanguage( 'nl' ); - $this->assertEquals( $derived->getDirection(), 'ltr' ); + $this->assertSame( $derived->getLanguage(), 'nl' ); + $this->assertSame( $derived->getDirection(), 'ltr' ); + // Changing the language must clear cache of computed direction $derived->setLanguage( 'he' ); - $this->assertEquals( $derived->getDirection(), 'rtl' ); + $this->assertSame( $derived->getDirection(), 'rtl' ); + $this->assertSame( $derived->getLanguage(), 'he' ); + // Overriding the direction explicitly is allowed $derived->setDirection( 'ltr' ); - $this->assertEquals( $derived->getDirection(), 'ltr' ); + $this->assertSame( $derived->getDirection(), 'ltr' ); + $this->assertSame( $derived->getLanguage(), 'he' ); } - public function testSkin() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeSkin() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getSkin(), 'fallback', 'inherit from parent' ); - $derived->setSkin( 'override' ); - $this->assertEquals( $derived->getSkin(), 'override' ); + $derived->setSkin( 'myskin' ); + $this->assertSame( $derived->getSkin(), 'myskin' ); } - public function testUser() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeUser() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getUser(), null, 'inherit from parent' ); - $derived->setUser( 'Example' ); - $this->assertEquals( $derived->getUser(), 'Example' ); + $derived->setUser( 'MyUser' ); + $this->assertSame( $derived->getUser(), 'MyUser' ); } - public function testDebug() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeDebug() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getDebug(), false, 'inherit from parent' ); $derived->setDebug( true ); - $this->assertEquals( $derived->getDebug(), true ); + $this->assertSame( $derived->getDebug(), true ); } - public function testOnly() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeOnly() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getOnly(), 'scripts', 'inherit from parent' ); $derived->setOnly( 'styles' ); - $this->assertEquals( $derived->getOnly(), 'styles' ); + $this->assertSame( $derived->getOnly(), 'styles' ); $derived->setOnly( null ); - $this->assertEquals( $derived->getOnly(), null ); + $this->assertSame( $derived->getOnly(), null ); } - public function testVersion() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeVersion() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getVersion(), null ); $derived->setVersion( 'hw1' ); - $this->assertEquals( $derived->getVersion(), 'hw1' ); + $this->assertSame( $derived->getVersion(), 'hw1' ); } - public function testRaw() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeRaw() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getRaw(), false, 'inherit from parent' ); $derived->setRaw( true ); - $this->assertEquals( $derived->getRaw(), true ); + $this->assertSame( $derived->getRaw(), true ); } - public function testGetHash() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); - - $this->assertEquals( $derived->getHash(), 'zh|fallback|||scripts|||||' ); + public function testChangeHash() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getHash(), 'qqx|fallback|||scripts|||||', 'inherit' ); $derived->setLanguage( 'nl' ); $derived->setUser( 'Example' ); // Assert that subclass is able to clear parent class "hash" member - $this->assertEquals( $derived->getHash(), 'nl|fallback||Example|scripts|||||' ); + $this->assertSame( $derived->getHash(), 'nl|fallback||Example|scripts|||||' ); } - public function testContentOverrides() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); - - $this->assertNull( $derived->getContentOverrideCallback() ); + public function testChangeContentOverrides() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertNull( $derived->getContentOverrideCallback(), 'default' ); $override = function ( Title $t ) { return null; }; $derived->setContentOverrideCallback( $override ); - $this->assertSame( $override, $derived->getContentOverrideCallback() ); + $this->assertSame( $override, $derived->getContentOverrideCallback(), 'changed' ); $derived2 = new DerivativeResourceLoaderContext( $derived ); - $this->assertSame( $override, $derived2->getContentOverrideCallback() ); + $this->assertSame( + $override, + $derived2->getContentOverrideCallback(), + 'change via a second derivative layer' + ); } - public function testAccessors() { - $context = self::getContext(); + public function testImmutableAccessors() { + $context = self::makeContext(); $derived = new DerivativeResourceLoaderContext( $context ); $this->assertSame( $derived->getRequest(), $context->getRequest() ); $this->assertSame( $derived->getResourceLoader(), $context->getResourceLoader() ); diff --git a/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php index 58e6d7d345..e57764306e 100644 --- a/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php +++ b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php @@ -3,73 +3,27 @@ use Wikimedia\TestingAccessWrapper; /** - * @group Cache + * @group ResourceLoader * @covers MessageBlobStore */ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase { use MediaWikiCoversValidator; + use PHPUnit4And6Compat; protected function setUp() { parent::setUp(); - // MediaWiki tests defaults $wgMainWANCache to CACHE_NONE. - // Use hash instead so that caching is observed - $this->wanCache = $this->getMockBuilder( WANObjectCache::class ) - ->setConstructorArgs( [ [ - 'cache' => new HashBagOStuff(), - 'pool' => 'test', - 'relayer' => new EventRelayerNull( [] ) - ] ] ) - ->setMethods( [ 'makePurgeValue' ] ) - ->getMock(); - - $this->wanCache->expects( $this->any() ) - ->method( 'makePurgeValue' ) - ->will( $this->returnCallback( function ( $timestamp, $holdoff ) { - // Disable holdoff as it messes with testing. Aside from a 0-second holdoff, - // make sure that "time" passes between getMulti() check init and the set() - // in recacheMessageBlob(). This especially matters for Windows clocks. - $ts = (float)$timestamp - 0.0001; - - return WANObjectCache::PURGE_VAL_PREFIX . $ts . ':0'; - } ) ); - } - - protected function makeBlobStore( $methods = null, $rl = null ) { - $blobStore = $this->getMockBuilder( MessageBlobStore::class ) - ->setConstructorArgs( [ $rl ] ) - ->setMethods( $methods ) - ->getMock(); - - $access = TestingAccessWrapper::newFromObject( $blobStore ); - $access->wanCache = $this->wanCache; - return $blobStore; - } - - protected function makeModule( array $messages ) { - $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] ); - $module->setName( 'test.blobstore' ); - return $module; - } - - /** @covers MessageBlobStore::setLogger */ - public function testSetLogger() { - $blobStore = $this->makeBlobStore(); - $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) ); + // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE. + // Use HashBagOStuff here so that we can observe caching. + $this->wanCache = new WANObjectCache( [ + 'cache' => new HashBagOStuff() + ] ); + + $this->clock = 1301655600.000; + $this->wanCache->setMockTime( $this->clock ); } - /** @covers MessageBlobStore::getResourceLoader */ - public function testGetResourceLoader() { - // Call protected method - $blobStore = TestingAccessWrapper::newFromObject( $this->makeBlobStore() ); - $this->assertInstanceOf( - ResourceLoader::class, - $blobStore->getResourceLoader() - ); - } - - /** @covers MessageBlobStore::fetchMessage */ - public function testFetchMessage() { + public function testBlobCreation() { $module = $this->makeModule( [ 'mainpage' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); @@ -80,140 +34,153 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase { $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' ); } - /** @covers MessageBlobStore::fetchMessage */ - public function testFetchMessageFail() { + public function testBlobCreation_unknownMessage() { $module = $this->makeModule( [ 'i-dont-exist' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( null, $rl ); - $blob = $blobStore->getBlob( $module, 'en' ); + // Generating a blob should succeed without errors, + // even if a message is unknown. + $blob = $blobStore->getBlob( $module, 'en' ); $this->assertEquals( '{"i-dont-exist":"\u29fci-dont-exist\u29fd"}', $blob, 'Generated blob' ); } - public function testGetBlob() { - $module = $this->makeModule( [ 'foo' ] ); + public function testMessageCachingAndPurging() { + $module = $this->makeModule( [ 'example' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); + + // Advance this new WANObjectCache instance to a normal state, + // by doing one "get" and letting its hold off period expire. + // Without this, the first real "get" would lazy-initialise the + // checkKey and thus reject the first "set". + $blobStore->getBlob( $module, 'en' ); + $this->clock += 20; + + // Arrange version 1 of a message $blobStore->expects( $this->once() ) ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'Example' ) ); + ->will( $this->returnValue( 'First version' ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); + $this->assertEquals( '{"example":"First version"}', $blob, 'Blob for v1' ); - $this->assertEquals( '{"foo":"Example"}', $blob, 'Generated blob' ); - } - - public function testGetBlobCached() { - $module = $this->makeModule( [ 'example' ] ); - $rl = new ResourceLoader(); - $rl->register( $module->getName(), $module ); - + // Arrange version 2 $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); $blobStore->expects( $this->once() ) ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'First' ) ); + ->will( $this->returnValue( 'Second version' ) ); + $this->clock += 20; - $module = $this->makeModule( [ 'example' ] ); + // Assert + // We do not validate whether a cached message is up-to-date. + // Instead, changes to messages will send us a purge. + // When cache is not purged or expired, it must be used. $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); + $this->assertEquals( '{"example":"First version"}', $blob, 'Reuse cached v1 blob' ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->never() ) - ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'Second' ) ); + // Purge cache + $blobStore->updateMessage( 'example' ); + $this->clock += 20; - $module = $this->makeModule( [ 'example' ] ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Cache hit' ); + $this->assertEquals( '{"example":"Second version"}', $blob, 'Updated blob for v2' ); } - public function testUpdateMessage() { + public function testPurgeEverything() { $module = $this->makeModule( [ 'example' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->once() ) + // Advance this new WANObjectCache instance to a normal state. + $blobStore->getBlob( $module, 'en' ); + $this->clock += 20; + + // Arrange version 1 and 2 + $blobStore->expects( $this->exactly( 2 ) ) ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'First' ) ); + ->will( $this->onConsecutiveCalls( 'First', 'Second' ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); + $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1' ); - $blobStore->updateMessage( 'example' ); + $this->clock += 20; - $module = $this->makeModule( [ 'example' ] ); - $rl = new ResourceLoader(); - $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->once() ) - ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'Second' ) ); + // Assert + $blob = $blobStore->getBlob( $module, 'en' ); + $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1 again' ); + + // Purge everything + $blobStore->clear(); + $this->clock += 20; + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' ); + $this->assertEquals( '{"example":"Second"}', $blob, 'Blob for v2' ); } - public function testValidation() { + public function testValidateAgainstModuleRegistry() { + // Arrange version 1 of a module $module = $this->makeModule( [ 'foo' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); $blobStore->expects( $this->once() ) ->method( 'fetchMessage' ) ->will( $this->returnValueMap( [ + // message key, language code, message value [ 'foo', 'en', 'Hello' ], ] ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"foo":"Hello"}', $blob, 'Generated blob' ); + $this->assertEquals( '{"foo":"Hello"}', $blob, 'Blob for v1' ); - // Now, imagine a change to the module is deployed. The module now contains - // message 'foo' and 'bar'. While updateMessage() was not called (since no - // message values were changed) it should detect the change in list of - // message keys. + // Arrange version 2 of module + // While message values may be out of date, the set of messages returned + // must always match the set of message keys required by the module. + // We do not receive purges for this because no messages were changed. $module = $this->makeModule( [ 'foo', 'bar' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); $blobStore->expects( $this->exactly( 2 ) ) ->method( 'fetchMessage' ) ->will( $this->returnValueMap( [ + // message key, language code, message value [ 'foo', 'en', 'Hello' ], [ 'bar', 'en', 'World' ], ] ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Updated blob' ); + $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Blob for v2' ); } - public function testClear() { - $module = $this->makeModule( [ 'example' ] ); - $rl = new ResourceLoader(); - $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->exactly( 2 ) ) - ->method( 'fetchMessage' ) - ->will( $this->onConsecutiveCalls( 'First', 'Second' ) ); - - $now = microtime( true ); - $this->wanCache->setMockTime( $now ); - - $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); + public function testSetLoggedIsVoid() { + $blobStore = $this->makeBlobStore(); + $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) ); + } - $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' ); + private function makeBlobStore( $methods = null, $rl = null ) { + $blobStore = $this->getMockBuilder( MessageBlobStore::class ) + ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] ) + ->setMethods( $methods ) + ->getMock(); - $now += 1; - $blobStore->clear(); + $access = TestingAccessWrapper::newFromObject( $blobStore ); + $access->wanCache = $this->wanCache; + return $blobStore; + } - $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' ); + private function makeModule( array $messages ) { + $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] ); + $module->setName( 'test.blobstore' ); + return $module; } } diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php index dbc757f90b..9ab3a2dae3 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php @@ -4,112 +4,12 @@ use Wikimedia\TestingAccessWrapper; /** * @group ResourceLoader + * @covers ResourceLoaderClientHtml */ class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase { use MediaWikiCoversValidator; - protected static function expandVariables( $text ) { - return strtr( $text, [ - '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION - ] ); - } - - protected static function makeContext( $extraQuery = [] ) { - $conf = new HashConfig( [ - 'ResourceLoaderSources' => [], - 'ResourceModuleSkinStyles' => [], - 'ResourceModules' => [], - 'EnableJavaScriptTest' => false, - 'ResourceLoaderDebug' => false, - 'LoadScript' => '/w/load.php', - ] ); - return new ResourceLoaderContext( - new ResourceLoader( $conf ), - new FauxRequest( array_merge( [ - 'lang' => 'nl', - 'skin' => 'fallback', - 'user' => 'Example', - 'target' => 'phpunit', - ], $extraQuery ) ) - ); - } - - protected static function makeModule( array $options = [] ) { - return new ResourceLoaderTestModule( $options ); - } - - protected static function makeSampleModules() { - $modules = [ - 'test' => [], - 'test.private' => [ 'group' => 'private' ], - 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ], - 'test.shouldembed' => [ 'shouldEmbed' => true ], - 'test.user' => [ 'group' => 'user' ], - - 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ], - 'test.styles.mixed' => [], - 'test.styles.noscript' => [ - 'type' => ResourceLoaderModule::LOAD_STYLES, - 'group' => 'noscript', - ], - 'test.styles.user' => [ - 'type' => ResourceLoaderModule::LOAD_STYLES, - 'group' => 'user', - ], - 'test.styles.user.empty' => [ - 'type' => ResourceLoaderModule::LOAD_STYLES, - 'group' => 'user', - 'isKnownEmpty' => true, - ], - 'test.styles.private' => [ - 'type' => ResourceLoaderModule::LOAD_STYLES, - 'group' => 'private', - 'styles' => '.private{}', - ], - 'test.styles.shouldembed' => [ - 'type' => ResourceLoaderModule::LOAD_STYLES, - 'shouldEmbed' => true, - 'styles' => '.shouldembed{}', - ], - 'test.styles.deprecated' => [ - 'type' => ResourceLoaderModule::LOAD_STYLES, - 'deprecated' => 'Deprecation message.', - ], - - 'test.scripts' => [], - 'test.scripts.user' => [ 'group' => 'user' ], - 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ], - 'test.scripts.raw' => [ 'isRaw' => true ], - 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ], - - 'test.ordering.a' => [ 'shouldEmbed' => false ], - 'test.ordering.b' => [ 'shouldEmbed' => false ], - 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ], - 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ], - 'test.ordering.e' => [ 'shouldEmbed' => false ], - ]; - return array_map( function ( $options ) { - return self::makeModule( $options ); - }, $modules ); - } - - /** - * @covers ResourceLoaderClientHtml::getDocumentAttributes - */ - public function testGetDocumentAttributes() { - $client = new ResourceLoaderClientHtml( self::makeContext() ); - $this->assertInternalType( 'array', $client->getDocumentAttributes() ); - } - - /** - * @covers ResourceLoaderClientHtml::__construct - * @covers ResourceLoaderClientHtml::setModules - * @covers ResourceLoaderClientHtml::setModuleStyles - * @covers ResourceLoaderClientHtml::setModuleScripts - * @covers ResourceLoaderClientHtml::getData - * @covers ResourceLoaderClientHtml::getContext - */ public function testGetData() { $context = self::makeContext(); $context->getResourceLoader()->register( self::makeSampleModules() ); @@ -132,29 +32,30 @@ class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase { 'test.styles.deprecated', 'test.unregistered.styles', ] ); - $client->setModuleScripts( [ - 'test.scripts', - 'test.scripts.user', - 'test.scripts.user.empty', - 'test.scripts.shouldembed', - 'test.unregistered.scripts', - ] ); $expected = [ 'states' => [ + // The below are NOT queued for loading via `mw.loader.load(Array)`. + // Instead we tell the client to set their state to "loading" so that + // if they are needed as dependencies, the client will not try to + // load them on-demand, because the server is taking care of them already. + // Either: + // - Embedded as inline scripts in the HTML (e.g. user-private code, and + // previews). Once that script tag is reached, the state is "loaded". + // - Loaded directly from the HTML with a dedicated HTTP request (e.g. + // user scripts, which vary by a 'user' and 'version' parameter that + // the static user-agnostic startup module won't have). 'test.private' => 'loading', - 'test.shouldembed.empty' => 'ready', 'test.shouldembed' => 'loading', 'test.user' => 'loading', + // The below are known to the server to be empty scripts, or to be + // synchronously loaded stylesheets. These start in the "ready" state. + 'test.shouldembed.empty' => 'ready', 'test.styles.pure' => 'ready', 'test.styles.user.empty' => 'ready', 'test.styles.private' => 'ready', 'test.styles.shouldembed' => 'ready', 'test.styles.deprecated' => 'ready', - 'test.scripts' => 'loading', - 'test.scripts.user' => 'loading', - 'test.scripts.user.empty' => 'ready', - 'test.scripts.shouldembed' => 'loading', ], 'general' => [ 'test', @@ -163,11 +64,6 @@ class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase { 'test.styles.pure', 'test.styles.deprecated', ], - 'scripts' => [ - 'test.scripts', - 'test.scripts.user', - 'test.scripts.shouldembed', - ], 'embed' => [ 'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ], 'general' => [ @@ -189,13 +85,6 @@ Deprecation message.' ] $this->assertEquals( $expected, $access->getData() ); } - /** - * @covers ResourceLoaderClientHtml::setConfig - * @covers ResourceLoaderClientHtml::setExemptStates - * @covers ResourceLoaderClientHtml::getHeadHtml - * @covers ResourceLoaderClientHtml::getLoad - * @covers ResourceLoader::makeLoaderStateScript - */ public function testGetHeadHtml() { $context = self::makeContext(); $context->getResourceLoader()->register( self::makeSampleModules() ); @@ -213,9 +102,6 @@ Deprecation message.' ] 'test.styles.private', 'test.styles.deprecated', ] ); - $client->setModuleScripts( [ - 'test.scripts', - ] ); $client->setExemptStates( [ 'test.exempt' => 'ready', ] ); @@ -224,24 +110,21 @@ Deprecation message.' ] $expected = '' . "\n" . '' . "\n" - . '' . "\n" + . '' . "\n" . '' . "\n" - . ''; + . ''; // phpcs:enable $expected = self::expandVariables( $expected ); - $this->assertEquals( $expected, $client->getHeadHtml() ); + $this->assertSame( $expected, (string)$client->getHeadHtml() ); } /** * Confirm that 'target' is passed down to the startup module's load url. - * - * @covers ResourceLoaderClientHtml::getHeadHtml */ public function testGetHeadHtmlWithTarget() { $client = new ResourceLoaderClientHtml( @@ -251,16 +134,14 @@ Deprecation message.' ] // phpcs:disable Generic.Files.LineLength $expected = '' . "\n" - . ''; + . ''; // phpcs:enable - $this->assertEquals( $expected, $client->getHeadHtml() ); + $this->assertSame( $expected, (string)$client->getHeadHtml() ); } /** * Confirm that 'safemode' is passed down to startup. - * - * @covers ResourceLoaderClientHtml::getHeadHtml */ public function testGetHeadHtmlWithSafemode() { $client = new ResourceLoaderClientHtml( @@ -270,16 +151,14 @@ Deprecation message.' ] // phpcs:disable Generic.Files.LineLength $expected = '' . "\n" - . ''; + . ''; // phpcs:enable - $this->assertEquals( $expected, $client->getHeadHtml() ); + $this->assertSame( $expected, (string)$client->getHeadHtml() ); } /** * Confirm that a null 'target' is the same as no target. - * - * @covers ResourceLoaderClientHtml::getHeadHtml */ public function testGetHeadHtmlWithNullTarget() { $client = new ResourceLoaderClientHtml( @@ -289,16 +168,12 @@ Deprecation message.' ] // phpcs:disable Generic.Files.LineLength $expected = '' . "\n" - . ''; + . ''; // phpcs:enable - $this->assertEquals( $expected, $client->getHeadHtml() ); + $this->assertSame( $expected, (string)$client->getHeadHtml() ); } - /** - * @covers ResourceLoaderClientHtml::getBodyHtml - * @covers ResourceLoaderClientHtml::getLoad - */ public function testGetBodyHtml() { $context = self::makeContext(); $context->getResourceLoader()->register( self::makeSampleModules() ); @@ -312,16 +187,13 @@ Deprecation message.' ] $client->setModuleStyles( [ 'test.styles.deprecated', ] ); - $client->setModuleScripts( [ - 'test.scripts', - ] ); // phpcs:disable Generic.Files.LineLength $expected = ''; // phpcs:enable - $this->assertEquals( $expected, $client->getBodyHtml() ); + $this->assertSame( $expected, (string)$client->getBodyHtml() ); } public static function provideMakeLoad() { @@ -331,104 +203,120 @@ Deprecation message.' ] 'context' => [], 'modules' => [ 'test.unknown' ], 'only' => ResourceLoaderModule::TYPE_STYLES, + 'extra' => [], 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.styles.private' ], 'only' => ResourceLoaderModule::TYPE_STYLES, + 'extra' => [], 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.private' ], 'only' => ResourceLoaderModule::TYPE_COMBINED, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ 'context' => [], // Eg. startup module 'modules' => [ 'test.scripts.raw' ], 'only' => ResourceLoaderModule::TYPE_SCRIPTS, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ - 'context' => [ 'sync' => true ], + 'context' => [], 'modules' => [ 'test.scripts.raw' ], 'only' => ResourceLoaderModule::TYPE_SCRIPTS, - 'output' => '', + 'extra' => [ 'sync' => '1' ], + 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.scripts.user' ], 'only' => ResourceLoaderModule::TYPE_SCRIPTS, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.user' ], 'only' => ResourceLoaderModule::TYPE_COMBINED, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ - 'context' => [ 'debug' => true ], + 'context' => [ 'debug' => 'true' ], 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ], 'only' => ResourceLoaderModule::TYPE_STYLES, + 'extra' => [], 'output' => '' . "\n" . '', ], [ - 'context' => [ 'debug' => false ], + 'context' => [ 'debug' => 'false' ], 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ], 'only' => ResourceLoaderModule::TYPE_STYLES, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.styles.noscript' ], 'only' => ResourceLoaderModule::TYPE_STYLES, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.shouldembed' ], 'only' => ResourceLoaderModule::TYPE_COMBINED, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.styles.shouldembed' ], 'only' => ResourceLoaderModule::TYPE_STYLES, + 'extra' => [], 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.scripts.shouldembed' ], 'only' => ResourceLoaderModule::TYPE_SCRIPTS, + 'extra' => [], 'output' => '', ], [ 'context' => [], 'modules' => [ 'test', 'test.shouldembed' ], 'only' => ResourceLoaderModule::TYPE_COMBINED, - 'output' => '', + 'extra' => [], + 'output' => '', ], [ 'context' => [], 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ], 'only' => ResourceLoaderModule::TYPE_STYLES, + 'extra' => [], 'output' => - '' . "\n" + '' . "\n" . '' ], [ 'context' => [], 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ], 'only' => ResourceLoaderModule::TYPE_STYLES, + 'extra' => [], 'output' => - '' . "\n" + '' . "\n" . '' . "\n" - . '' + . '' ], ]; // phpcs:enable @@ -436,21 +324,109 @@ Deprecation message.' ] /** * @dataProvider provideMakeLoad - * @covers ResourceLoaderClientHtml::makeLoad - * @covers ResourceLoaderClientHtml::makeContext - * @covers ResourceLoader::makeModuleResponse + * @covers ResourceLoaderClientHtml * @covers ResourceLoaderModule::getModuleContent - * @covers ResourceLoader::getCombinedVersion - * @covers ResourceLoader::createLoaderURL - * @covers ResourceLoader::createLoaderQuery - * @covers ResourceLoader::makeLoaderQuery - * @covers ResourceLoader::makeInlineScript + * @covers ResourceLoader */ - public function testMakeLoad( array $extraQuery, array $modules, $type, $expected ) { - $context = self::makeContext( $extraQuery ); + public function testMakeLoad( + array $contextQuery, + array $modules, + $type, + array $extraQuery, + $expected + ) { + $context = self::makeContext( $contextQuery ); $context->getResourceLoader()->register( self::makeSampleModules() ); $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false ); $expected = self::expandVariables( $expected ); - $this->assertEquals( $expected, (string)$actual ); + $this->assertSame( $expected, (string)$actual ); + } + + public function testGetDocumentAttributes() { + $client = new ResourceLoaderClientHtml( self::makeContext() ); + $this->assertInternalType( 'array', $client->getDocumentAttributes() ); + } + + private static function expandVariables( $text ) { + return strtr( $text, [ + '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION + ] ); + } + + private static function makeContext( $extraQuery = [] ) { + $conf = new HashConfig( [ + 'ResourceModuleSkinStyles' => [], + 'ResourceModules' => [], + 'EnableJavaScriptTest' => false, + 'LoadScript' => '/w/load.php', + ] ); + return new ResourceLoaderContext( + new ResourceLoader( $conf ), + new FauxRequest( array_merge( [ + 'lang' => 'nl', + 'skin' => 'fallback', + 'user' => 'Example', + 'target' => 'phpunit', + ], $extraQuery ) ) + ); + } + + private static function makeModule( array $options = [] ) { + return new ResourceLoaderTestModule( $options ); + } + + private static function makeSampleModules() { + $modules = [ + 'test' => [], + 'test.private' => [ 'group' => 'private' ], + 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ], + 'test.shouldembed' => [ 'shouldEmbed' => true ], + 'test.user' => [ 'group' => 'user' ], + + 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ], + 'test.styles.mixed' => [], + 'test.styles.noscript' => [ + 'type' => ResourceLoaderModule::LOAD_STYLES, + 'group' => 'noscript', + ], + 'test.styles.user' => [ + 'type' => ResourceLoaderModule::LOAD_STYLES, + 'group' => 'user', + ], + 'test.styles.user.empty' => [ + 'type' => ResourceLoaderModule::LOAD_STYLES, + 'group' => 'user', + 'isKnownEmpty' => true, + ], + 'test.styles.private' => [ + 'type' => ResourceLoaderModule::LOAD_STYLES, + 'group' => 'private', + 'styles' => '.private{}', + ], + 'test.styles.shouldembed' => [ + 'type' => ResourceLoaderModule::LOAD_STYLES, + 'shouldEmbed' => true, + 'styles' => '.shouldembed{}', + ], + 'test.styles.deprecated' => [ + 'type' => ResourceLoaderModule::LOAD_STYLES, + 'deprecated' => 'Deprecation message.', + ], + + 'test.scripts' => [], + 'test.scripts.user' => [ 'group' => 'user' ], + 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ], + 'test.scripts.raw' => [ 'isRaw' => true ], + 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ], + + 'test.ordering.a' => [ 'shouldEmbed' => false ], + 'test.ordering.b' => [ 'shouldEmbed' => false ], + 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ], + 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ], + 'test.ordering.e' => [ 'shouldEmbed' => false ], + ]; + return array_map( function ( $options ) { + return self::makeModule( $options ); + }, $modules ); } } diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php index 20d4b54cd4..0a4cf1e108 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php @@ -320,7 +320,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase { $testModule = new ResourceLoaderFileModule( [ 'localBasePath' => $basePath, 'styles' => [ 'bom.css' ], - ] ); + ] ); $testModule->setName( 'testing' ); $this->assertEquals( substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ), @@ -372,4 +372,207 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase { 'Using less variables is significant' ); } + + public function providerGetScriptPackageFiles() { + $basePath = __DIR__ . '/../../data/resourceloader'; + $base = [ 'localBasePath' => $basePath ]; + $commentScript = file_get_contents( "$basePath/script-comment.js" ); + $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" ); + $config = RequestContext::getMain()->getConfig(); + return [ + [ + $base + [ + 'packageFiles' => [ + 'script-comment.js', + 'script-nosemi.js' + ] + ], + [ + 'files' => [ + 'script-comment.js' => [ + 'type' => 'script', + 'content' => $commentScript, + ], + 'script-nosemi.js' => [ + 'type' => 'script', + 'content' => $nosemiScript + ] + ], + 'main' => 'script-comment.js' + ] + ], + [ + $base + [ + 'packageFiles' => [ + 'script-comment.js', + [ 'name' => 'script-nosemi.js', 'main' => true ] + ], + 'deprecated' => 'Deprecation test', + 'name' => 'test-deprecated' + ], + [ + 'files' => [ + 'script-comment.js' => [ + 'type' => 'script', + 'content' => $commentScript, + ], + 'script-nosemi.js' => [ + 'type' => 'script', + 'content' => 'mw.log.warn(' . + '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' . + "Deprecation test" . + '");' . + $nosemiScript + ] + ], + 'main' => 'script-nosemi.js' + ] + ], + [ + $base + [ + 'packageFiles' => [ + [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ], + [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ], + ] + ], + [ + 'files' => [ + 'init.js' => [ + 'type' => 'script', + 'content' => $commentScript, + ], + 'nosemi.js' => [ + 'type' => 'script', + 'content' => $nosemiScript + ] + ], + 'main' => 'init.js' + ] + ], + [ + $base + [ + 'packageFiles' => [ + [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ], + 'sample.json', + [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ], + [ 'name' => 'data.json', 'callback' => function ( $context ) { + return [ 'langCode' => $context->getLanguage() ]; + } ], + [ 'name' => 'config.json', 'config' => [ + 'Sitename', + 'wgVersion' => 'Version', + ] ], + ] + ], + [ + 'files' => [ + 'foo.json' => [ + 'type' => 'data', + 'content' => [ 'Hello' => 'world' ], + ], + 'sample.json' => [ + 'type' => 'data', + 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ], + ], + 'bar.js' => [ + 'type' => 'script', + 'content' => "console.log('Hello');", + ], + 'data.json' => [ + 'type' => 'data', + 'content' => [ 'langCode' => 'fy' ] + ], + 'config.json' => [ + 'type' => 'data', + 'content' => [ + 'Sitename' => $config->get( 'Sitename' ), + 'wgVersion' => $config->get( 'Version' ), + ] + ] + ], + 'main' => 'bar.js' + ], + [ + 'lang' => 'fy' + ] + ], + [ + $base + [ + 'packageFiles' => [ + [ 'file' => 'script-comment.js' ] + ] + ], + false + ], + [ + $base + [ + 'packageFiles' => [ + [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ] + ] + ], + false + ], + [ + $base + [ + 'packageFiles' => [ + 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ] + ] + ], + false + ], + [ + $base + [ + 'packageFiles' => [ + [ 'name' => 'foo.js', 'config' => 'Sitename' ] + ] + ], + false + ], + [ + $base + [ + 'packageFiles' => [ + 'foo.js' => [ 'garbage' => 'data' ] + ] + ], + false + ], + [ + $base + [ + 'packageFiles' => [ + 'filethatdoesnotexist142857.js' + ] + ], + false + ], + [ + $base + [ + 'packageFiles' => [ + 'script-nosemi.js', + [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ] + ] + ], + false + ] + ]; + } + + /** + * @dataProvider providerGetScriptPackageFiles + * @covers ResourceLoaderFileModule::getScript + * @covers ResourceLoaderFileModule::getPackageFiles + * @covers ResourceLoaderFileModule::expandPackageFiles + */ + public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) { + $module = new ResourceLoaderFileModule( $moduleDefinition ); + $context = $this->getResourceLoaderContext( $contextOptions ); + if ( isset( $moduleDefinition['name'] ) ) { + $module->setName( $moduleDefinition['name'] ); + } + if ( $expected === false ) { + $this->setExpectedException( MWException::class ); + $module->getScript( $context ); + } else { + $this->assertEquals( $expected, $module->getScript( $context ) ); + } + } } diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php index 2ee85b5ee5..d4b5ed6c91 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php @@ -459,13 +459,12 @@ mw.loader.register( [ * @covers ResourceLoader::makeLoaderRegisterScript */ public function testGetModuleRegistrations( $case ) { - if ( isset( $case['sources'] ) ) { - $this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] ); - } - $extraQuery = $case['extraQuery'] ?? []; $context = $this->getResourceLoaderContext( $extraQuery ); $rl = $context->getResourceLoader(); + if ( isset( $case['sources'] ) ) { + $rl->addSource( $case['sources'] ); + } $rl->register( $case['modules'] ); $module = new ResourceLoaderStartUpModule(); $out = ltrim( $case['out'], "\n" ); diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php index 32afd75033..3f7925f914 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php @@ -148,10 +148,6 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { 'SkinModule (FileModule subclass)' => [ true, [ 'class' => ResourceLoaderSkinModule::class, 'scripts' => 'example.js' ] ], - 'JqueryMsgModule (FileModule subclass)' => [ true, [ - 'class' => ResourceLoaderJqueryMsgModule::class, - 'scripts' => 'example.js', - ] ], 'WikiModule' => [ false, [ 'class' => ResourceLoaderWikiModule::class, 'scripts' => [ 'MediaWiki:Example.js' ], @@ -435,6 +431,45 @@ mw.example(); 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );', ] ], + [ [ + 'title' => 'Implement multi-file script', + + 'name' => 'test.multifile', + 'scripts' => [ + 'files' => [ + 'one.js' => [ + 'type' => 'script', + 'content' => 'mw.example( 1 );', + ], + 'two.json' => [ + 'type' => 'data', + 'content' => [ 'n' => 2 ], + ], + 'three.js' => [ + 'type' => 'script', + 'content' => 'mw.example( 3 );' + ], + ], + 'main' => 'three.js', + ], + + 'expected' => << true, - 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ) + 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [], ]; ResourceLoader::clearCache(); $this->setMwGlobals( 'wgResourceLoaderDebug', true ); @@ -461,7 +496,8 @@ mw.example(); : $case['scripts'], $case['styles'], $case['messages'], - $case['templates'] + $case['templates'], + $case['packageFiles'] ) ); } @@ -477,7 +513,8 @@ mw.example(); 123, // scripts null, // styles null, // messages - null // templates + null, // templates + null // package files ); } @@ -587,7 +624,6 @@ mw.example(); * @covers ResourceLoader::getLoadScript */ public function testGetLoadScript() { - $this->setMwGlobals( 'wgResourceLoaderSources', [] ); $rl = new ResourceLoader(); $sources = self::fakeSources(); $rl->addSource( $sources ); @@ -730,11 +766,11 @@ mw.example(); }, $scripts ); $rl->register( $modules ); - $this->setMwGlobals( 'wgResourceLoaderDebug', $debug ); $context = $this->getResourceLoaderContext( [ 'modules' => implode( '|', array_keys( $modules ) ), 'only' => 'scripts', + 'debug' => $debug ? 'true' : 'false', ], $rl ); diff --git a/tests/phpunit/includes/search/SearchEnginePrefixTest.php b/tests/phpunit/includes/search/SearchEnginePrefixTest.php index ee272b9b5d..372cb33cbb 100644 --- a/tests/phpunit/includes/search/SearchEnginePrefixTest.php +++ b/tests/phpunit/includes/search/SearchEnginePrefixTest.php @@ -382,6 +382,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase { /** * @dataProvider paginationProvider + * @covers SearchSuggestionSet::hasMoreResults */ public function testPagination( $hasMoreResults, $provision ) { $search = $this->mockSearchWithResults( $provision ); diff --git a/tests/phpunit/includes/search/SearchSuggestionSetTest.php b/tests/phpunit/includes/search/SearchSuggestionSetTest.php index 54533a7333..02fa5e9cac 100644 --- a/tests/phpunit/includes/search/SearchSuggestionSetTest.php +++ b/tests/phpunit/includes/search/SearchSuggestionSetTest.php @@ -23,6 +23,7 @@ class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase { /** * Test that adding a new suggestion at the end * will keep proper score ordering + * @covers SearchSuggestionSet::append */ public function testAppend() { $set = SearchSuggestionSet::emptySuggestionSet(); @@ -54,6 +55,9 @@ class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase { /** * Test that adding a new best suggestion will keep proper score * ordering + * @covers SearchSuggestionSet::getWorstScore + * @covers SearchSuggestionSet::getBestScore + * @covers SearchSuggestionSet::prepend */ public function testInsertBest() { $set = SearchSuggestionSet::emptySuggestionSet(); @@ -88,6 +92,9 @@ class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase { $this->assertEquals( $sorted, $scores ); } + /** + * @covers SearchSuggestionSet::shrink + */ public function testShrink() { $set = SearchSuggestionSet::emptySuggestionSet(); for ( $i = 0; $i < 100; $i++ ) { diff --git a/tests/phpunit/includes/session/SessionProviderTest.php b/tests/phpunit/includes/session/SessionProviderTest.php index 052c0167a9..6ff6a97b8f 100644 --- a/tests/phpunit/includes/session/SessionProviderTest.php +++ b/tests/phpunit/includes/session/SessionProviderTest.php @@ -134,7 +134,7 @@ class SessionProviderTest extends MediaWikiTestCase { $this->fail( 'Expected exception not thrown' ); } catch ( \BadMethodCallException $ex ) { $this->assertSame( - 'MediaWiki\\Session\\SessionProvider::preventSessionsForUser must be implmented ' . + 'MediaWiki\\Session\\SessionProvider::preventSessionsForUser must be implemented ' . 'when canChangeUser() is false', $ex->getMessage() ); diff --git a/tests/phpunit/includes/session/TestUtils.php b/tests/phpunit/includes/session/TestUtils.php index ef8fb4ea7e..1d87c9036f 100644 --- a/tests/phpunit/includes/session/TestUtils.php +++ b/tests/phpunit/includes/session/TestUtils.php @@ -85,11 +85,6 @@ class TestUtils { */ public static function getDummySession( $backend = null, $index = -1, $logger = null ) { $rc = new \ReflectionClass( Session::class ); - if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) { - \PHPUnit_Framework_Assert::markTestSkipped( - 'ReflectionClass::newInstanceWithoutConstructor isn\'t available' - ); - } if ( $backend === null ) { $backend = new DummySessionBackend; diff --git a/tests/phpunit/includes/site/DBSiteStoreTest.php b/tests/phpunit/includes/site/DBSiteStoreTest.php index da6e9f9305..14ee15b635 100644 --- a/tests/phpunit/includes/site/DBSiteStoreTest.php +++ b/tests/phpunit/includes/site/DBSiteStoreTest.php @@ -3,8 +3,6 @@ use MediaWiki\MediaWikiServices; /** - * Tests for the DBSiteStore 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 diff --git a/tests/phpunit/includes/site/FileBasedSiteLookupTest.php b/tests/phpunit/includes/site/FileBasedSiteLookupTest.php deleted file mode 100644 index 69e0e38936..0000000000 --- a/tests/phpunit/includes/site/FileBasedSiteLookupTest.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ -class FileBasedSiteLookupTest extends PHPUnit\Framework\TestCase { - - use MediaWikiCoversValidator; - - protected function setUp() { - $this->cacheFile = $this->getCacheFile(); - } - - protected function tearDown() { - unlink( $this->cacheFile ); - } - - public function testGetSites() { - $sites = $this->getSites(); - $cacheBuilder = $this->newSitesCacheFileBuilder( $sites ); - $cacheBuilder->build(); - - $cache = new FileBasedSiteLookup( $this->cacheFile ); - $this->assertEquals( $sites, $cache->getSites() ); - } - - public function testGetSite() { - $sites = $this->getSites(); - $cacheBuilder = $this->newSitesCacheFileBuilder( $sites ); - $cacheBuilder->build(); - - $cache = new FileBasedSiteLookup( $this->cacheFile ); - - $this->assertEquals( $sites->getSite( 'enwiktionary' ), $cache->getSite( 'enwiktionary' ) ); - } - - private function newSitesCacheFileBuilder( SiteList $sites ) { - return new SitesCacheFileBuilder( - $this->getSiteLookup( $sites ), - $this->cacheFile - ); - } - - private function getSiteLookup( SiteList $sites ) { - $siteLookup = $this->getMockBuilder( SiteLookup::class ) - ->disableOriginalConstructor() - ->getMock(); - - $siteLookup->expects( $this->any() ) - ->method( 'getSites' ) - ->will( $this->returnValue( $sites ) ); - - return $siteLookup; - } - - private function getSites() { - $sites = []; - - $site = new Site(); - $site->setGlobalId( 'foobar' ); - $sites[] = $site; - - $site = new MediaWikiSite(); - $site->setGlobalId( 'enwiktionary' ); - $site->setGroup( 'wiktionary' ); - $site->setLanguageCode( 'en' ); - $site->addNavigationId( 'enwiktionary' ); - $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" ); - $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" ); - $sites[] = $site; - - return new SiteList( $sites ); - } - - private function getCacheFile() { - return tempnam( sys_get_temp_dir(), 'mw-test-sitelist' ); - } - -} diff --git a/tests/phpunit/includes/site/MediaWikiSiteTest.php b/tests/phpunit/includes/site/MediaWikiSiteTest.php index b367979946..a9732d1c3d 100644 --- a/tests/phpunit/includes/site/MediaWikiSiteTest.php +++ b/tests/phpunit/includes/site/MediaWikiSiteTest.php @@ -1,8 +1,6 @@ setMwGlobals( [ 'wgCapitalLinks' => true, diff --git a/tests/phpunit/includes/site/SiteExporterTest.php b/tests/phpunit/includes/site/SiteExporterTest.php index db900da914..97a43f8d5b 100644 --- a/tests/phpunit/includes/site/SiteExporterTest.php +++ b/tests/phpunit/includes/site/SiteExporterTest.php @@ -1,8 +1,6 @@ - */ -class SitesCacheFileBuilderTest extends PHPUnit\Framework\TestCase { - - use MediaWikiCoversValidator; - - protected function setUp() { - $this->cacheFile = $this->getCacheFile(); - } - - protected function tearDown() { - unlink( $this->cacheFile ); - } - - public function testBuild() { - $cacheBuilder = $this->newSitesCacheFileBuilder( $this->getSites() ); - $cacheBuilder->build(); - - $contents = file_get_contents( $this->cacheFile ); - $this->assertEquals( json_encode( $this->getExpectedData() ), $contents ); - } - - private function getExpectedData() { - return [ - 'sites' => [ - 'foobar' => [ - 'globalid' => 'foobar', - 'type' => 'unknown', - 'group' => 'none', - 'source' => 'local', - 'language' => null, - 'localids' => [], - 'config' => [], - 'data' => [], - 'forward' => false, - 'internalid' => null, - 'identifiers' => [] - ], - 'enwiktionary' => [ - 'globalid' => 'enwiktionary', - 'type' => 'mediawiki', - 'group' => 'wiktionary', - 'source' => 'local', - 'language' => 'en', - 'localids' => [ - 'equivalent' => [ 'enwiktionary' ] - ], - 'config' => [], - 'data' => [ - 'paths' => [ - 'page_path' => 'https://en.wiktionary.org/wiki/$1', - 'file_path' => 'https://en.wiktionary.org/w/$1' - ] - ], - 'forward' => false, - 'internalid' => null, - 'identifiers' => [ - [ - 'type' => 'equivalent', - 'key' => 'enwiktionary' - ] - ] - ] - ] - ]; - } - - private function newSitesCacheFileBuilder( SiteList $sites ) { - return new SitesCacheFileBuilder( - $this->getSiteLookup( $sites ), - $this->cacheFile - ); - } - - private function getSiteLookup( SiteList $sites ) { - $siteLookup = $this->getMockBuilder( SiteLookup::class ) - ->disableOriginalConstructor() - ->getMock(); - - $siteLookup->expects( $this->any() ) - ->method( 'getSites' ) - ->will( $this->returnValue( $sites ) ); - - return $siteLookup; - } - - private function getSites() { - $sites = []; - - $site = new Site(); - $site->setGlobalId( 'foobar' ); - $sites[] = $site; - - $site = new MediaWikiSite(); - $site->setGlobalId( 'enwiktionary' ); - $site->setGroup( 'wiktionary' ); - $site->setLanguageCode( 'en' ); - $site->addNavigationId( 'enwiktionary' ); - $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" ); - $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" ); - $sites[] = $site; - - return new SiteList( $sites ); - } - - private function getCacheFile() { - return tempnam( sys_get_temp_dir(), 'mw-test-sitelist' ); - } - -} diff --git a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php index 584b141c2c..2ce097b5be 100644 --- a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php +++ b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php @@ -613,8 +613,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase 'Learner1', 'Learner2', 'Learner3', 'Learner4', 'Experienced1', ], - $this->fetchUsers( [ 'learner', 'experienced' ], $now ), - 'Learner and more experienced' + $this->fetchUsers( [ 'learner', 'experienced' ], $now ) ); } diff --git a/tests/phpunit/includes/specialpage/FormSpecialPageTestCase.php b/tests/phpunit/includes/specialpage/FormSpecialPageTestCase.php new file mode 100644 index 0000000000..a3b5adb858 --- /dev/null +++ b/tests/phpunit/includes/specialpage/FormSpecialPageTestCase.php @@ -0,0 +1,79 @@ +newSpecialPage(); + $checkExecutePermissions = $this->getMethod( $special, 'checkExecutePermissions' ); + + $user = clone $this->getTestUser()->getUser(); + $user->mBlockedby = $user->getName(); + $user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $user->getId(), + 'reason' => 'sitewide block', + 'timestamp' => time(), + 'sitewide' => true, + 'expiry' => 10, + ] ); + + $this->expectException( UserBlockedError::class ); + $checkExecutePermissions( $user ); + } + + /** + * @covers FormSpecialPage::checkExecutePermissions + */ + public function testCheckExecutePermissionsPartialBlock() { + $special = $this->newSpecialPage(); + $checkExecutePermissions = $this->getMethod( $special, 'checkExecutePermissions' ); + + $user = clone $this->getTestUser()->getUser(); + $user->mBlockedby = $user->getName(); + $user->mBlock = new Block( [ + 'address' => '127.0.8.1', + 'by' => $user->getId(), + 'reason' => 'partial block', + 'timestamp' => time(), + 'sitewide' => false, + 'expiry' => 10, + ] ); + + $this->assertNull( $checkExecutePermissions( $user ) ); + } + + /** + * Get a protected/private method. + * + * @param object $obj + * @param string $name + * @return callable + */ + protected function getMethod( $obj, $name ) { + $method = new ReflectionMethod( $obj, $name ); + $method->setAccessible( true ); + return $method->getClosure( $obj ); + } +} diff --git a/tests/phpunit/includes/specialpage/SpecialPageTest.php b/tests/phpunit/includes/specialpage/SpecialPageTest.php index 2eddb01e8e..ec4bf0fc43 100644 --- a/tests/phpunit/includes/specialpage/SpecialPageTest.php +++ b/tests/phpunit/includes/specialpage/SpecialPageTest.php @@ -1,5 +1,7 @@ assertTrue( true ); } + public function provideBuildPrevNextNavigation() { + yield [ 0, 20, false, false ]; + yield [ 17, 20, false, false ]; + yield [ 0, 17, false, false ]; + yield [ 0, 20, true, 'Foo' ]; + yield [ 17, 20, true, 'Föö_Bär' ]; + } + + /** + * @dataProvider provideBuildPrevNextNavigation + */ + public function testBuildPrevNextNavigation( $offset, $limit, $atEnd, $subPage ) { + $this->setUserLang( Language::factory( 'qqx' ) ); // disable i18n + + $specialPage = new SpecialPage( 'Watchlist' ); + $specialPage = TestingAccessWrapper::newFromObject( $specialPage ); + + $html = $specialPage->buildPrevNextNavigation( + $offset, + $limit, + [ 'x' => 25 ], + $atEnd, + $subPage + ); + + $this->assertStringStartsWith( '(viewprevnext:', $html ); + + preg_match_all( '!!', $html, $m, PREG_PATTERN_ORDER ); + $links = $m[0]; + + foreach ( $links as $a ) { + if ( $subPage ) { + $this->assertContains( 'Special:Watchlist/' . wfUrlencode( $subPage ), $a ); + } else { + $this->assertContains( 'Special:Watchlist', $a ); + $this->assertNotContains( 'Special:Watchlist/', $a ); + } + $this->assertContains( 'x=25', $a ); + } + + $i = 0; + + if ( $offset > 0 ) { + $this->assertContains( + 'limit=' . $limit . '&offset=' . max( 0, $offset - $limit ) . '&', + $links[ $i ] + ); + $this->assertContains( 'title="(prevn-title: ' . $limit . ')"', $links[$i] ); + $this->assertContains( 'class="mw-prevlink"', $links[$i] ); + $this->assertContains( '>(prevn: ' . $limit . ')<', $links[$i] ); + $i += 1; + } + + if ( !$atEnd ) { + $this->assertContains( + 'limit=' . $limit . '&offset=' . ( $offset + $limit ) . '&', + $links[ $i ] + ); + $this->assertContains( 'title="(nextn-title: ' . $limit . ')"', $links[$i] ); + $this->assertContains( 'class="mw-nextlink"', $links[$i] ); + $this->assertContains( '>(nextn: ' . $limit . ')<', $links[$i] ); + $i += 1; + } + + $this->assertCount( 5 + $i, $links ); + + $this->assertContains( 'limit=20&offset=' . $offset, $links[$i] ); + $this->assertContains( 'title="(shown-title: 20)"', $links[$i] ); + $this->assertContains( 'class="mw-numlink"', $links[$i] ); + $this->assertContains( '>20<', $links[$i] ); + $i += 4; + + $this->assertContains( 'limit=500&offset=' . $offset, $links[$i] ); + $this->assertContains( 'title="(shown-title: 500)"', $links[$i] ); + $this->assertContains( 'class="mw-numlink"', $links[$i] ); + $this->assertContains( '>500<', $links[$i] ); + } + } diff --git a/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php index 4a171dfac0..58f83decba 100644 --- a/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php +++ b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php @@ -42,8 +42,7 @@ class QueryAllSpecialPagesTest extends MediaWikiTestCase { parent::__construct(); foreach ( QueryPage::getPages() as $page ) { - $class = $page[0]; - $name = $page[1]; + list( $class, $name ) = $page; if ( !in_array( $class, $this->manualTest ) ) { $this->queryPages[$class] = MediaWikiServices::getInstance()->getSpecialPageFactory()->getPage( $name ); diff --git a/tests/phpunit/includes/specials/SpecialBlockTest.php b/tests/phpunit/includes/specials/SpecialBlockTest.php index 55a8b66e57..182ca0d043 100644 --- a/tests/phpunit/includes/specials/SpecialBlockTest.php +++ b/tests/phpunit/includes/specials/SpecialBlockTest.php @@ -12,7 +12,7 @@ use Wikimedia\TestingAccessWrapper; */ class SpecialBlockTest extends SpecialPageTestBase { /** - * {@inheritdoc} + * @inheritDoc */ protected function newSpecialPage() { return new SpecialBlock(); @@ -29,6 +29,7 @@ class SpecialBlockTest extends SpecialPageTestBase { public function testGetFormFields() { $this->setMwGlobals( [ 'wgEnablePartialBlocks' => false, + 'wgBlockAllowsUTEdit' => true, ] ); $page = $this->newSpecialPage(); $wrappedPage = TestingAccessWrapper::newFromObject( $page ); @@ -71,6 +72,7 @@ class SpecialBlockTest extends SpecialPageTestBase { public function testMaybeAlterFormDefaults() { $this->setMwGlobals( [ 'wgEnablePartialBlocks' => false, + 'wgBlockAllowsUTEdit' => true, ] ); $block = $this->insertBlock(); @@ -86,10 +88,10 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertSame( (string)$block->getTarget(), $fields['Target']['default'] ); $this->assertSame( $block->isHardblock(), $fields['HardBlock']['default'] ); - $this->assertSame( $block->prevents( 'createaccount' ), $fields['CreateAccount']['default'] ); + $this->assertSame( $block->isCreateAccountBlocked(), $fields['CreateAccount']['default'] ); $this->assertSame( $block->isAutoblocking(), $fields['AutoBlock']['default'] ); - $this->assertSame( $block->prevents( 'editownusertalk' ), $fields['DisableUTEdit']['default'] ); - $this->assertSame( $block->mReason, $fields['Reason']['default'] ); + $this->assertSame( !$block->isUsertalkEditAllowed(), $fields['DisableUTEdit']['default'] ); + $this->assertSame( $block->getReason(), $fields['Reason']['default'] ); $this->assertSame( 'infinite', $fields['Expiry']['default'] ); } @@ -119,6 +121,8 @@ class SpecialBlockTest extends SpecialPageTestBase { new PageRestriction( 0, $pageSaturn->getId() ), new PageRestriction( 0, $pageMars->getId() ), new NamespaceRestriction( 0, NS_TALK ), + // Deleted page. + new PageRestriction( 0, 999999 ), ] ); $block->insert(); @@ -175,7 +179,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); } @@ -224,7 +228,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); $this->assertSame( '1', $block->isAutoblocking() ); } @@ -273,7 +277,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); $this->assertCount( 2, $block->getRestrictions() ); $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [ @@ -327,7 +331,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); $this->assertFalse( $block->isSitewide() ); $this->assertCount( 2, $block->getRestrictions() ); @@ -343,7 +347,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); $this->assertFalse( $block->isSitewide() ); $this->assertCount( 1, $block->getRestrictions() ); @@ -358,7 +362,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); $this->assertFalse( $block->isSitewide() ); $this->assertCount( 0, $block->getRestrictions() ); @@ -370,7 +374,7 @@ class SpecialBlockTest extends SpecialPageTestBase { $this->assertTrue( $result ); $block = Block::newFromTarget( $badActor ); - $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $reason, $block->getReason() ); $this->assertSame( $expiry, $block->getExpiry() ); $this->assertTrue( $block->isSitewide() ); $this->assertCount( 0, $block->getRestrictions() ); @@ -397,6 +401,9 @@ class SpecialBlockTest extends SpecialPageTestBase { $expectedResult, $reason ) { + $this->setMwGlobals( [ + 'wgBlockDisablesLogin' => false, + ] ); $this->setGroupPermissions( 'sysop', 'unblockself', true ); $this->setGroupPermissions( 'user', 'block', true ); // Getting errors about creating users in db in provider. diff --git a/tests/phpunit/includes/specials/SpecialPasswordResetTest.php b/tests/phpunit/includes/specials/SpecialPasswordResetTest.php new file mode 100644 index 0000000000..273b428f4a --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialPasswordResetTest.php @@ -0,0 +1,10 @@ + on search result page * https://gerrit.wikimedia.org/r/4841 + * @covers SpecialSearch::setupPage */ public function testSearchTermIsNotExpanded() { $this->setMwGlobals( [ @@ -175,6 +176,7 @@ class SpecialSearchTest extends MediaWikiTestCase { /** * @dataProvider provideRewriteQueryWithSuggestion + * @covers SpecialSearch::showResults */ public function testRewriteQueryWithSuggestion( $message, @@ -224,6 +226,9 @@ class SpecialSearchTest extends MediaWikiTestCase { return $mock; } + /** + * @covers SpecialSearch::execute + */ public function testSubPageRedirect() { $this->setMwGlobals( [ 'wgScript' => '/w/index.php', diff --git a/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php b/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php deleted file mode 100644 index 80bd365f35..0000000000 --- a/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php +++ /dev/null @@ -1,63 +0,0 @@ -getMockBuilder( RequestContext::class )->getMock(); - $mockContext->method( 'msg' )->willReturn( $msg ); - $special = new UncategorizedCategoriesPage(); - $special->setContext( $mockContext ); - $this->assertEquals( [ - 'tables' => [ - 0 => 'page', - 1 => 'categorylinks', - ], - 'fields' => [ - 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_title', - ], - 'conds' => [ - 0 => 'cl_from IS NULL', - 'page_namespace' => 14, - 'page_is_redirect' => 0, - ] + $expected, - 'join_conds' => [ - 'categorylinks' => [ - 0 => 'LEFT JOIN', - 1 => 'cl_from = page_id', - ], - ], - ], $special->getQueryInfo() ); - } - - public function provideTestGetQueryInfoData() { - return [ - [ - "* Stubs\n* Test\n* *\n* * test123", - [ 1 => "page_title not in ( 'Stubs','Test','*','*_test123' )" ] - ], - [ - "Stubs\n* Test\n* *\n* * test123", - [ 1 => "page_title not in ( 'Test','*','*_test123' )" ] - ], - [ - "* StubsTest\n* *\n* * test123", - [ 1 => "page_title not in ( 'StubsTest','*','*_test123' )" ] - ], - [ "", [] ], - [ "\n\n\n", [] ], - [ "\n", [] ], - [ "Test\n*Test2", [ 1 => "page_title not in ( 'Test2' )" ] ], - [ "Test", [] ], - [ "*Test\nTest2", [ 1 => "page_title not in ( 'Test' )" ] ], - [ "Test\nTest2", [] ], - ]; - } -} diff --git a/tests/phpunit/includes/specials/SpecialWatchlistTest.php b/tests/phpunit/includes/specials/SpecialWatchlistTest.php index 28e26a082b..642ae3e2de 100644 --- a/tests/phpunit/includes/specials/SpecialWatchlistTest.php +++ b/tests/phpunit/includes/specials/SpecialWatchlistTest.php @@ -59,7 +59,44 @@ class SpecialWatchlistTest extends SpecialPageTestBase { /** * @dataProvider provideFetchOptionsFromRequest */ - public function testFetchOptionsFromRequest( $expectedValues, $preferences, $inputParams ) { + public function testFetchOptionsFromRequest( + $expectedValuesDefaults, $expectedValues, $preferences, $inputParams + ) { + // $defaults and $allFalse are just to make the expected values below + // shorter by hiding the background. + + $page = TestingAccessWrapper::newFromObject( + $this->newSpecialPage() + ); + + $page->registerFilters(); + + // Does not consider $preferences, just wiki's defaults + $wikiDefaults = $page->getDefaultOptions()->getAllValues(); + + switch ( $expectedValuesDefaults ) { + case 'allFalse': + $allFalse = $wikiDefaults; + + foreach ( $allFalse as $key => $value ) { + if ( $value === true ) { + $allFalse[$key] = false; + } + } + + // This is not exposed on the form (only in preferences) so it + // respects the preference. + $allFalse['extended'] = true; + + $expectedValues += $allFalse; + break; + case 'wikiDefaults': + $expectedValues += $wikiDefaults; + break; + default: + $this->fail( "Unknown \$expectedValuesDefaults: $expectedValuesDefaults" ); + } + $page = TestingAccessWrapper::newFromObject( $this->newSpecialPage() ); @@ -90,43 +127,21 @@ class SpecialWatchlistTest extends SpecialPageTestBase { } public function provideFetchOptionsFromRequest() { - // $defaults and $allFalse are just to make the expected values below - // shorter by hiding the background. - - $page = TestingAccessWrapper::newFromObject( - $this->newSpecialPage() - ); - - $page->registerFilters(); - - // Does not consider $preferences, just wiki's defaults - $wikiDefaults = $page->getDefaultOptions()->getAllValues(); - - $allFalse = $wikiDefaults; - - foreach ( $allFalse as $key => &$value ) { - if ( $value === true ) { - $value = false; - } - } - - // This is not exposed on the form (only in preferences) so it - // respects the preference. - $allFalse['extended'] = true; - return [ - [ - [ + 'ignores casing' => [ + 'expectedValuesDefaults' => 'wikiDefaults', + 'expectedValues' => [ 'hideminor' => true, - ] + $wikiDefaults, - [], - [ + ], + 'preferences' => [], + 'inputParams' => [ 'hideMinor' => 1, ], ], - [ - [ + 'first two same as prefs, second two overriden' => [ + 'expectedValuesDefaults' => 'wikiDefaults', + 'expectedValues' => [ // First two same as prefs 'hideminor' => true, 'hidebots' => false, @@ -135,38 +150,38 @@ class SpecialWatchlistTest extends SpecialPageTestBase { 'hideanons' => false, 'hideliu' => true, 'userExpLevel' => 'registered' - ] + $wikiDefaults, - [ + ], + 'preferences' => [ 'watchlisthideminor' => 1, 'watchlisthidebots' => 0, 'watchlisthideanons' => 1, 'watchlisthideliu' => 0, ], - [ + 'inputParams' => [ 'hideanons' => 0, 'hideliu' => 1, ], ], - // Defaults/preferences for form elements are entirely ignored for - // action=submit and omitted elements become false - [ - [ + 'Defaults/preferences for form elements are entirely ignored for ' + . 'action=submit and omitted elements become false' => [ + 'expectedValuesDefaults' => 'allFalse', + 'expectedValues' => [ 'hideminor' => false, 'hidebots' => true, 'hideanons' => false, 'hideliu' => true, 'userExpLevel' => 'unregistered' - ] + $allFalse, - [ + ], + 'preferences' => [ 'watchlisthideminor' => 0, 'watchlisthidebots' => 1, 'watchlisthideanons' => 0, 'watchlisthideliu' => 1, ], - [ + 'inputParams' => [ 'hidebots' => 1, 'hideliu' => 1, 'action' => 'submit', diff --git a/tests/phpunit/includes/specials/UncategorizedCategoriesPageTest.php b/tests/phpunit/includes/specials/UncategorizedCategoriesPageTest.php new file mode 100644 index 0000000000..80bd365f35 --- /dev/null +++ b/tests/phpunit/includes/specials/UncategorizedCategoriesPageTest.php @@ -0,0 +1,63 @@ +getMockBuilder( RequestContext::class )->getMock(); + $mockContext->method( 'msg' )->willReturn( $msg ); + $special = new UncategorizedCategoriesPage(); + $special->setContext( $mockContext ); + $this->assertEquals( [ + 'tables' => [ + 0 => 'page', + 1 => 'categorylinks', + ], + 'fields' => [ + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title', + ], + 'conds' => [ + 0 => 'cl_from IS NULL', + 'page_namespace' => 14, + 'page_is_redirect' => 0, + ] + $expected, + 'join_conds' => [ + 'categorylinks' => [ + 0 => 'LEFT JOIN', + 1 => 'cl_from = page_id', + ], + ], + ], $special->getQueryInfo() ); + } + + public function provideTestGetQueryInfoData() { + return [ + [ + "* Stubs\n* Test\n* *\n* * test123", + [ 1 => "page_title not in ( 'Stubs','Test','*','*_test123' )" ] + ], + [ + "Stubs\n* Test\n* *\n* * test123", + [ 1 => "page_title not in ( 'Test','*','*_test123' )" ] + ], + [ + "* StubsTest\n* *\n* * test123", + [ 1 => "page_title not in ( 'StubsTest','*','*_test123' )" ] + ], + [ "", [] ], + [ "\n\n\n", [] ], + [ "\n", [] ], + [ "Test\n*Test2", [ 1 => "page_title not in ( 'Test2' )" ] ], + [ "Test", [] ], + [ "*Test\nTest2", [ 1 => "page_title not in ( 'Test' )" ] ], + [ "Test\nTest2", [] ], + ]; + } +} diff --git a/tests/phpunit/includes/specials/pagers/BlockListPagerTest.php b/tests/phpunit/includes/specials/pagers/BlockListPagerTest.php index 80df1d0866..bd37c04cd5 100644 --- a/tests/phpunit/includes/specials/pagers/BlockListPagerTest.php +++ b/tests/phpunit/includes/specials/pagers/BlockListPagerTest.php @@ -1,6 +1,7 @@ setMwGlobals( [ 'wgEnablePartialBlocks' => false, ] ); + // Set the time to now so it does not get off during the test. + MWTimestamp::setFakeTime( MWTimestamp::time() ); + + $value = $name === 'ipb_timestamp' ? MWTimestamp::time() : ''; + $expected = $expected ?? MWTimestamp::getInstance()->format( 'H:i, j F Y' ); + $row = $row ?: new stdClass; $pager = new BlockListPager( new SpecialPage(), [] ); $wrappedPager = TestingAccessWrapper::newFromObject( $pager ); @@ -29,6 +35,9 @@ class BlockListPagerTest extends MediaWikiTestCase { $formatted = $pager->formatValue( $name, $value ); $this->assertEquals( $expected, $formatted ); + + // Reset the time. + MWTimestamp::setFakeTime( false ); } /** @@ -38,17 +47,13 @@ class BlockListPagerTest extends MediaWikiTestCase { return [ [ 'test', - '', 'Unable to format test', ], [ 'ipb_timestamp', - wfTimestamp( TS_UNIX ), - date( 'H:i, j F Y' ), ], [ 'ipb_expiry', - '', 'infinite
0 minutes left', ], ]; @@ -76,31 +81,26 @@ class BlockListPagerTest extends MediaWikiTestCase { return [ [ 'test', - '', 'Unable to format test', $row, ], [ 'ipb_timestamp', - wfTimestamp( TS_UNIX ), - date( 'H:i, j F Y' ), + null, $row, ], [ 'ipb_expiry', - '', 'infinite
0 minutes left', $row, ], [ 'ipb_by', - '', $row->ipb_by_text, $row, ], [ 'ipb_params', - '', '
  • account creation disabled
  • cannot edit own talk page
', $row, ] @@ -109,9 +109,13 @@ class BlockListPagerTest extends MediaWikiTestCase { /** * @covers ::formatValue + * @covers ::getRestrictionListHTML */ public function testFormatValueRestrictions() { - $this->setMwGlobals( 'wgArticlePath', '/wiki/$1' ); + $this->setMwGlobals( [ + 'wgArticlePath' => '/wiki/$1', + 'wgScript' => '/w/index.php', + ] ); $pager = new BlockListPager( new SpecialPage(), [] ); @@ -134,7 +138,10 @@ class BlockListPagerTest extends MediaWikiTestCase { $pageId = $page['id']; $restrictions = [ - ( new PageRestriction( 0, $pageId ) )->setTitle( $title ) + ( new PageRestriction( 0, $pageId ) )->setTitle( $title ), + new NamespaceRestriction( 0, NS_MAIN ), + // Deleted page. + new PageRestriction( 0, 999999 ), ]; $wrappedPager = TestingAccessWrapper::newFromObject( $pager ); @@ -146,11 +153,21 @@ class BlockListPagerTest extends MediaWikiTestCase { // and must not depend on a localisation message. // TODO: Mock the message or consider using qqx. . wfMessage( 'blocklist-editing' )->text() - . '', + . '
  • ' + . wfMessage( 'blocklist-editing-ns' )->text() + . '
  • ', $formatted ); } @@ -229,7 +246,7 @@ class BlockListPagerTest extends MediaWikiTestCase { $restriction = $restrictions[0]; $this->assertEquals( $page->getId(), $restriction->getValue() ); - $this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleId() ); + $this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleID() ); $this->assertEquals( $title->getDBKey(), $restriction->getTitle()->getDBKey() ); $this->assertEquals( $title->getNamespace(), $restriction->getTitle()->getNamespace() ); diff --git a/tests/phpunit/includes/tidy/RemexDriverTest.php b/tests/phpunit/includes/tidy/RemexDriverTest.php index a5ebaa5ddc..5ad8416b81 100644 --- a/tests/phpunit/includes/tidy/RemexDriverTest.php +++ b/tests/phpunit/includes/tidy/RemexDriverTest.php @@ -1,8 +1,7 @@

    ', '

    ', ], + [ + 'style tag isn\'t p-wrapped (T186965)', + '', + '', + ], + [ + 'link tag isn\'t p-wrapped (T186965)', + '', + '', + ], + [ + 'style tag doesn\'t split p-wrapping (T208901)', + 'foo bar', + '

    foo bar

    ', + ], + [ + 'link tag doesn\'t split p-wrapping (T208901)', + 'foo bar', + '

    foo bar

    ', + ], ]; public function provider() { diff --git a/tests/phpunit/includes/user/PasswordResetTest.php b/tests/phpunit/includes/user/PasswordResetTest.php index 4978b7290a..e8334d6bf9 100644 --- a/tests/phpunit/includes/user/PasswordResetTest.php +++ b/tests/phpunit/includes/user/PasswordResetTest.php @@ -133,6 +133,15 @@ class PasswordResetTest extends MediaWikiTestCase { 'globalBlock' => null, 'isAllowed' => true, ], + 'blocked with an unknown system block type' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => new Block( [ 'systemBlock' => 'unknown' ] ), + 'globalBlock' => null, + 'isAllowed' => false, + ], 'all OK' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, diff --git a/tests/phpunit/includes/user/UserTest.php b/tests/phpunit/includes/user/UserTest.php index 84f9378788..f84be3f1c4 100644 --- a/tests/phpunit/includes/user/UserTest.php +++ b/tests/phpunit/includes/user/UserTest.php @@ -583,6 +583,7 @@ class UserTest extends MediaWikiTestCase { * When a user is autoblocked a cookie is set with which to track them * in case they log out and change IP addresses. * @link https://phabricator.wikimedia.org/T5233 + * @covers User::trackBlockWithCookie */ public function testAutoblockCookies() { // Set up the bits of global configuration that we use. @@ -598,7 +599,6 @@ class UserTest extends MediaWikiTestCase { ] ); // 1. Log in a test user, and block them. - $userBlocker = $this->getTestSysop()->getUser(); $user1tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1tmp ); @@ -609,7 +609,6 @@ class UserTest extends MediaWikiTestCase { ] ); $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1tmp ); - $block->setBlocker( $userBlocker ); $res = $block->insert(); $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); @@ -665,6 +664,7 @@ class UserTest extends MediaWikiTestCase { /** * Make sure that no cookie is set to track autoblocked users * when $wgCookieSetOnAutoblock is false. + * @covers User::trackBlockWithCookie */ public function testAutoblockCookiesDisabled() { // Set up the bits of global configuration that we use. @@ -680,14 +680,12 @@ class UserTest extends MediaWikiTestCase { ] ); // 1. Log in a test user, and block them. - $userBlocker = $this->getTestSysop()->getUser(); $testUser = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $testUser ); $block = new Block( [ 'enableAutoblock' => true ] ); $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $testUser ); - $block->setBlocker( $userBlocker ); $res = $block->insert(); $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user = User::newFromSession( $request1 ); @@ -712,6 +710,7 @@ class UserTest extends MediaWikiTestCase { * When a user is autoblocked and a cookie is set to track them, the expiry time of the cookie * should match the block's expiry, to a maximum of 24 hours. If the expiry time is changed, * the cookie's should change with it. + * @covers User::trackBlockWithCookie */ public function testAutoblockCookieInfiniteExpiry() { $this->setMwGlobals( [ @@ -726,14 +725,12 @@ class UserTest extends MediaWikiTestCase { ] ); // 1. Log in a test user, and block them indefinitely. - $userBlocker = $this->getTestSysop()->getUser(); $user1Tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1Tmp ); $block = new Block( [ 'enableAutoblock' => true, 'expiry' => 'infinity' ] ); $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1Tmp ); - $block->setBlocker( $userBlocker ); $res = $block->insert(); $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); @@ -760,7 +757,7 @@ class UserTest extends MediaWikiTestCase { // 3. Change the block's expiry (to 2 hours), and the cookie's should be changed also. $newExpiry = wfTimestamp() + 2 * 60 * 60; - $block->mExpiry = wfTimestamp( TS_MW, $newExpiry ); + $block->setExpiry( wfTimestamp( TS_MW, $newExpiry ) ); $block->update(); $user2tmp = $this->getTestUser()->getUser(); $request2 = new FauxRequest(); @@ -776,37 +773,47 @@ class UserTest extends MediaWikiTestCase { $block->delete(); } + /** + * @covers User::getBlockedStatus + */ public function testSoftBlockRanges() { - global $wgUser; - - $this->setMwGlobals( [ - 'wgSoftBlockRanges' => [ '10.0.0.0/8' ], - 'wgUser' => null, - ] ); + $setSessionUser = function ( User $user, WebRequest $request ) { + $this->setMwGlobals( 'wgUser', $user ); + RequestContext::getMain()->setUser( $user ); + RequestContext::getMain()->setRequest( $request ); + TestingAccessWrapper::newFromObject( $user )->mRequest = $request; + $request->getSession()->setUser( $user ); + }; + $this->setMwGlobals( 'wgSoftBlockRanges', [ '10.0.0.0/8' ] ); // IP isn't in $wgSoftBlockRanges + $wgUser = new User(); $request = new FauxRequest(); $request->setIP( '192.168.0.1' ); - $wgUser = User::newFromSession( $request ); + $setSessionUser( $wgUser, $request ); $this->assertNull( $wgUser->getBlock() ); // IP is in $wgSoftBlockRanges + $wgUser = new User(); $request = new FauxRequest(); $request->setIP( '10.20.30.40' ); - $wgUser = User::newFromSession( $request ); + $setSessionUser( $wgUser, $request ); $block = $wgUser->getBlock(); $this->assertInstanceOf( Block::class, $block ); $this->assertSame( 'wgSoftBlockRanges', $block->getSystemBlockType() ); // Make sure the block is really soft - $request->getSession()->setUser( $this->getTestUser()->getUser() ); - $wgUser = User::newFromSession( $request ); + $wgUser = $this->getTestUser()->getUser(); + $request = new FauxRequest(); + $request->setIP( '10.20.30.40' ); + $setSessionUser( $wgUser, $request ); $this->assertFalse( $wgUser->isAnon(), 'sanity check' ); $this->assertNull( $wgUser->getBlock() ); } /** * Test that a modified BlockID cookie doesn't actually load the relevant block (T152951). + * @covers User::trackBlockWithCookie */ public function testAutoblockCookieInauthentic() { // Set up the bits of global configuration that we use. @@ -822,14 +829,12 @@ class UserTest extends MediaWikiTestCase { ] ); // 1. Log in a blocked test user. - $userBlocker = $this->getTestSysop()->getUser(); $user1tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1tmp ); $block = new Block( [ 'enableAutoblock' => true ] ); $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1tmp ); - $block->setBlocker( $userBlocker ); $res = $block->insert(); $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); @@ -853,6 +858,7 @@ class UserTest extends MediaWikiTestCase { /** * The BlockID cookie is normally verified with a HMAC, but not if wgSecretKey is not set. * This checks that a non-authenticated cookie still works. + * @covers User::trackBlockWithCookie */ public function testAutoblockCookieNoSecretKey() { // Set up the bits of global configuration that we use. @@ -868,14 +874,12 @@ class UserTest extends MediaWikiTestCase { ] ); // 1. Log in a blocked test user. - $userBlocker = $this->getTestSysop()->getUser(); $user1tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1tmp ); $block = new Block( [ 'enableAutoblock' => true ] ); $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1tmp ); - $block->setBlocker( $userBlocker ); $res = $block->insert(); $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); @@ -1022,6 +1026,9 @@ class UserTest extends MediaWikiTestCase { $this->assertTrue( User::isLocallyBlockedProxy( $ip ) ); } + /** + * @covers User::newFromActorId + */ public function testActorId() { $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID(); $this->hideDeprecated( 'User::selectFields' ); @@ -1085,6 +1092,9 @@ class UserTest extends MediaWikiTestCase { 'User::newFromActorId works for an anonymous user' ); } + /** + * @covers User::newFromAnyId + */ public function testNewFromAnyId() { // Registered user $user = $this->getTestUser()->getUser(); @@ -1271,17 +1281,18 @@ class UserTest extends MediaWikiTestCase { public static function provideIsBlockedFrom() { return [ - 'Basic operation' => [ 'Test page', true ], - 'User talk page, not allowed' => [ self::USER_TALK_PAGE, true, [ + 'Sitewide block, basic operation' => [ 'Test page', true ], + 'Sitewide block, not allowing user talk' => [ + self::USER_TALK_PAGE, true, [ 'allowUsertalk' => false, ] ], - 'User talk page, allowed' => [ - self::USER_TALK_PAGE, false, [ + 'Sitewide block, allowing user talk' => [ + self::USER_TALK_PAGE, false, [ 'allowUsertalk' => true, ] ], - 'User talk page, allowed but $wgBlockAllowsUTEdit is false' => [ + 'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [ self::USER_TALK_PAGE, true, [ 'allowUsertalk' => true, 'blockAllowsUTEdit' => false, @@ -1297,46 +1308,58 @@ class UserTest extends MediaWikiTestCase { 'pageRestrictions' => [ 'Test page' ], ] ], - 'Partial block, allowing user talk' => [ + 'Partial block, not allowing user talk but user talk page is not blocked' => [ self::USER_TALK_PAGE, false, [ 'allowUsertalk' => false, 'pageRestrictions' => [ 'Test page' ], ] ], - 'Partial block, not allowing user talk' => [ + 'Partial block, allowing user talk but user talk page is blocked' => [ self::USER_TALK_PAGE, true, [ 'allowUsertalk' => true, 'pageRestrictions' => [ self::USER_TALK_PAGE ], ] ], - 'Partial block, allowing user talk but $wgBlockAllowsUTEdit is false' => [ + 'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [ self::USER_TALK_PAGE, false, [ 'allowUsertalk' => false, 'pageRestrictions' => [ 'Test page' ], 'blockAllowsUTEdit' => false, ] ], - 'Partial block, not allowing user talk with $wgBlockAllowsUTEdit set to false' => [ + 'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [ self::USER_TALK_PAGE, true, [ 'allowUsertalk' => true, 'pageRestrictions' => [ self::USER_TALK_PAGE ], 'blockAllowsUTEdit' => false, ] ], - 'Partial namespace block, not allowing user talk' => [ self::USER_TALK_PAGE, true, [ - 'allowUsertalk' => false, - 'namespaceRestrictions' => [ NS_USER_TALK ], - ] ], - 'Partial namespace block, not allowing user talk' => [ self::USER_TALK_PAGE, false, [ - 'allowUsertalk' => true, - 'namespaceRestrictions' => [ NS_USER_TALK ], - ] ], + 'Partial user talk namespace block, not allowing user talk' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => false, + 'namespaceRestrictions' => [ NS_USER_TALK ], + ] + ], + 'Partial user talk namespace block, allowing user talk' => [ + self::USER_TALK_PAGE, false, [ + 'allowUsertalk' => true, + 'namespaceRestrictions' => [ NS_USER_TALK ], + ] + ], + 'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [ + self::USER_TALK_PAGE, true, [ + 'allowUsertalk' => true, + 'namespaceRestrictions' => [ NS_USER_TALK ], + 'blockAllowsUTEdit' => false, + ] + ], ]; } /** * Block cookie should be set for IP Blocks if * wgCookieSetOnIpBlock is set to true + * @covers User::trackBlockWithCookie */ public function testIpBlockCookieSet() { $this->setMwGlobals( [ @@ -1372,6 +1395,7 @@ class UserTest extends MediaWikiTestCase { /** * Block cookie should NOT be set when wgCookieSetOnIpBlock * is disabled + * @covers User::trackBlockWithCookie */ public function testIpBlockCookieNotSet() { $this->setMwGlobals( [ @@ -1407,6 +1431,7 @@ class UserTest extends MediaWikiTestCase { /** * When an ip user is blocked and then they log in, cookie block * should be invalid and the cookie removed. + * @covers User::trackBlockWithCookie */ public function testIpBlockCookieIgnoredWhenUserLoggedIn() { $this->setMwGlobals( [ @@ -1443,4 +1468,42 @@ class UserTest extends MediaWikiTestCase { // clean up $block->delete(); } + + /** + * @covers User::getFirstEditTimestamp + * @covers User::getLatestEditTimestamp + */ + public function testGetFirstLatestEditTimestamp() { + $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' ); + MWTimestamp::setFakeTime( function () use ( &$clock ) { + return $clock += 1000; + } ); + try { + $user = $this->getTestUser()->getUser(); + $firstRevision = self::makeEdit( $user, 'Help:UserTest_GetEditTimestamp', 'one', 'test' ); + $secondRevision = self::makeEdit( $user, 'Help:UserTest_GetEditTimestamp', 'two', 'test' ); + // Sanity check: revisions timestamp are different + $this->assertNotEquals( $firstRevision->getTimestamp(), $secondRevision->getTimestamp() ); + + $this->assertEquals( $firstRevision->getTimestamp(), $user->getFirstEditTimestamp() ); + $this->assertEquals( $secondRevision->getTimestamp(), $user->getLatestEditTimestamp() ); + } finally { + MWTimestamp::setFakeTime( false ); + } + } + + /** + * @param User $user + * @param string $title + * @param string $content + * @param string $comment + * @return \MediaWiki\Revision\RevisionRecord|null + */ + private static function makeEdit( User $user, $title, $content, $comment ) { + $page = WikiPage::factory( Title::newFromText( $title ) ); + $content = ContentHandler::makeContent( $content, $page->getTitle() ); + $updater = $page->newPageUpdater( $user ); + $updater->setContent( 'main', $content ); + return $updater->saveRevision( CommentStoreComment::newUnsavedComment( $comment ) ); + } } diff --git a/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php index 50e6c202f4..16f236770c 100644 --- a/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php +++ b/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php @@ -72,7 +72,8 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { return new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ), $this->getMockCommentStore(), - $this->getMockActorMigration() + $this->getMockActorMigration(), + $this->getMockWatchedItemStore() ); } @@ -139,6 +140,22 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { return $mock; } + /** + * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb + * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore + */ + private function getMockWatchedItemStore() { + $mock = $this->getMockBuilder( WatchedItemStore::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->any() ) + ->method( 'getLatestNotificationTimestamp' ) + ->will( $this->returnCallback( function ( $timestamp ) { + return $timestamp; + } ) ); + return $mock; + } + /** * @param int $id * @return PHPUnit_Framework_MockObject_MockObject|User @@ -263,7 +280,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { ], [ 'watchlist' => [ - 'INNER JOIN', + 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' @@ -386,7 +403,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { ], [ 'watchlist' => [ - 'INNER JOIN', + 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' @@ -888,7 +905,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { $expectedJoinConds = array_merge( [ 'watchlist' => [ - 'INNER JOIN', + 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' @@ -1121,7 +1138,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { $this->isType( 'string' ), $this->isType( 'array' ), array_merge( [ - 'watchlist' => [ 'INNER JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ], + 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ], 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ], ], $expectedExtraJoins ) ) @@ -1159,7 +1176,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { [], [ 'watchlist' => [ - 'INNER JOIN', + 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' @@ -1282,7 +1299,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { [], [ 'watchlist' => [ - 'INNER JOIN', + 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' @@ -1328,7 +1345,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { [], [ 'watchlist' => [ - 'INNER JOIN', + 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' diff --git a/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php b/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php index 3102929ec7..20dbedb50b 100644 --- a/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php +++ b/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php @@ -1,6 +1,7 @@ getNamespace() => [ $title->getDBkey() => null ] ], $store->getNotificationTimestampsBatch( $user, [ $title ] ) ); + + // Run the job queue + JobQueueGroup::destroySingletons(); + $jobs = new RunJobs; + $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null ); + $jobs->execute(); + $this->assertEquals( $initialVisitingWatchers, $store->countVisitingWatchers( $title, '20150202020202' ) @@ -192,19 +200,28 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase { // setNotificationTimestampsForUser specifying a title $this->assertTrue( - $store->setNotificationTimestampsForUser( $user, '20200202020202', [ $title ] ) + $store->setNotificationTimestampsForUser( $user, '20100202020202', [ $title ] ) ); $this->assertEquals( - '20200202020202', + '20100202020202', $store->getWatchedItem( $user, $title )->getNotificationTimestamp() ); // setNotificationTimestampsForUser not specifying a title + // This will try to use a DeferredUpdate; disable that + $mockCallback = function ( $callback ) { + $callback(); + }; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); $this->assertTrue( - $store->setNotificationTimestampsForUser( $user, '20210202020202' ) + $store->setNotificationTimestampsForUser( $user, '20110202020202' ) ); + // Because the operation above is normally deferred, it doesn't clear the cache + // Clear the cache manually + $wrappedStore = TestingAccessWrapper::newFromObject( $store ); + $wrappedStore->uncacheUser( $user ); $this->assertEquals( - '20210202020202', + '20110202020202', $store->getWatchedItem( $user, $title )->getNotificationTimestamp() ); } diff --git a/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php index 240b3f520f..a6b2162962 100644 --- a/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php +++ b/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php @@ -59,6 +59,26 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { return $mock; } + /** + * @return PHPUnit_Framework_MockObject_MockObject|JobQueueGroup + */ + private function getMockJobQueueGroup() { + $mock = $this->getMockBuilder( JobQueueGroup::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->any() ) + ->method( 'push' ) + ->will( $this->returnCallback( function ( Job $job ) { + $job->run(); + } ) ); + $mock->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( function ( Job $job ) { + $job->run(); + } ) ); + return $mock; + } + /** * @return PHPUnit_Framework_MockObject_MockObject|HashBagOStuff */ @@ -100,6 +120,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mock->expects( $this->any() ) ->method( 'getId' ) ->will( $this->returnValue( $id ) ); + $mock->expects( $this->any() ) + ->method( 'getUserPage' ) + ->will( $this->returnValue( Title::makeTitle( NS_USER, 'MockUser' ) ) ); return $mock; } @@ -118,11 +141,16 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { return $fakeRow; } - private function newWatchedItemStore( LBFactory $lbFactory, HashBagOStuff $cache, + private function newWatchedItemStore( + LBFactory $lbFactory, + JobQueueGroup $queueGroup, + HashBagOStuff $cache, ReadOnlyMode $readOnlyMode ) { return new WatchedItemStore( $lbFactory, + $queueGroup, + new HashBagOStuff(), $cache, $readOnlyMode, 1000 @@ -161,6 +189,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -193,6 +222,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -223,6 +253,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -254,6 +285,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -306,6 +338,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -373,6 +406,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -422,6 +456,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -504,6 +539,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -609,6 +645,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -663,6 +700,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -701,6 +739,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -736,6 +775,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -774,6 +814,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -805,6 +846,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode() ); @@ -864,6 +906,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -911,6 +954,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1005,6 +1049,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1038,6 +1083,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1059,6 +1105,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1072,6 +1119,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $this->getMockDb() ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode( true ) ); @@ -1122,6 +1170,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1147,6 +1196,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1171,6 +1221,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1206,6 +1257,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1241,6 +1293,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1264,6 +1317,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1313,6 +1367,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1364,6 +1419,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1389,6 +1445,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1434,6 +1491,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1469,6 +1527,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1507,6 +1566,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1531,6 +1591,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1572,6 +1633,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1623,6 +1685,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $mockLoadBalancer, + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1637,6 +1700,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { public function testGetWatchedItemsForUser_badSortOptionThrowsException() { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $this->getMockDb() ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode() ); @@ -1679,6 +1743,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1716,6 +1781,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1740,6 +1806,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1808,6 +1875,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1859,6 +1927,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1921,6 +1990,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1962,6 +2032,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -1989,6 +2060,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -2014,6 +2086,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -2048,6 +2121,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -2092,29 +2166,26 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); + $mockQueueGroup->expects( $this->once() ) + ->method( 'lazyPush' ) + ->willReturnCallback( function ( ActivityUpdateJob $job ) { + // don't run + } ); + $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - // Note: This does not actually assert the job is correct - $callableCallCounter = 0; - $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { - $callableCallCounter++; - $this->assertInternalType( 'callable', $callable ); - }; - $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); - $this->assertTrue( $store->resetNotificationTimestamp( $user, $title ) ); - $this->assertEquals( 1, $callableCallCounter ); - - ScopedCallback::consume( $scopedOverride ); } public function testResetNotificationTimestamp_noItemForced() { @@ -2132,19 +2203,19 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - // Note: This does not actually assert the job is correct - $callableCallCounter = 0; - $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { - $callableCallCounter++; - $this->assertInternalType( 'callable', $callable ); - }; - $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); + $mockQueueGroup->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( function ( ActivityUpdateJob $job ) { + // don't run + } ) ); $this->assertTrue( $store->resetNotificationTimestamp( @@ -2153,9 +2224,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { 'force' ) ); - $this->assertEquals( 1, $callableCallCounter ); - - ScopedCallback::consume( $scopedOverride ); } /** @@ -2179,20 +2247,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } private function verifyCallbackJob( - $callback, + ActivityUpdateJob $job, LinkTarget $expectedTitle, $expectedUserId, callable $notificationTimestampCondition ) { - $this->assertInternalType( 'callable', $callback ); - - $callbackReflector = new ReflectionFunction( $callback ); - $vars = $callbackReflector->getStaticVariables(); - $this->assertArrayHasKey( 'job', $vars ); - $this->assertInstanceOf( ActivityUpdateJob::class, $vars['job'] ); - - /** @var ActivityUpdateJob $job */ - $job = $vars['job']; $this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() ); $this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() ); @@ -2225,26 +2284,28 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeTitle:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - $callableCallCounter = 0; - $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( - function ( $callable ) use ( &$callableCallCounter, $title, $user ) { - $callableCallCounter++; - $this->verifyCallbackJob( - $callable, - $title, - $user->getId(), - function ( $time ) { - return $time === null; - } - ); - } - ); + $mockQueueGroup->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( + function ( ActivityUpdateJob $job ) use ( $title, $user ) { + $this->verifyCallbackJob( + $job, + $title, + $user->getId(), + function ( $time ) { + return $time === null; + } + ); + } + ) ); $this->assertTrue( $store->resetNotificationTimestamp( @@ -2254,9 +2315,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 1, $callableCallCounter ); - - ScopedCallback::consume( $scopedOverride ); } public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() { @@ -2293,26 +2351,28 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - $addUpdateCallCounter = 0; - $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback( - function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) { - $addUpdateCallCounter++; - $this->verifyCallbackJob( - $callable, - $title, - $user->getId(), - function ( $time ) { - return $time !== null && $time > '20151212010101'; - } - ); - } - ); + $mockQueueGroup->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( + function ( ActivityUpdateJob $job ) use ( $title, $user ) { + $this->verifyCallbackJob( + $job, + $title, + $user->getId(), + function ( $time ) { + return $time !== null && $time > '20151212010101'; + } + ); + } + ) ); $getTimestampCallCounter = 0; $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( @@ -2331,10 +2391,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 1, $addUpdateCallCounter ); - $this->assertEquals( 1, $getTimestampCallCounter ); + $this->assertEquals( 2, $getTimestampCallCounter ); - ScopedCallback::consume( $scopedOverrideDeferred ); ScopedCallback::consume( $scopedOverrideRevision ); } @@ -2368,26 +2426,28 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - $callableCallCounter = 0; - $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( - function ( $callable ) use ( &$callableCallCounter, $title, $user ) { - $callableCallCounter++; - $this->verifyCallbackJob( - $callable, - $title, - $user->getId(), - function ( $time ) { - return $time === null; - } - ); - } - ); + $mockQueueGroup->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( + function ( ActivityUpdateJob $job ) use ( $title, $user ) { + $this->verifyCallbackJob( + $job, + $title, + $user->getId(), + function ( $time ) { + return $time === null; + } + ); + } + ) ); $this->assertTrue( $store->resetNotificationTimestamp( @@ -2397,9 +2457,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 1, $callableCallCounter ); - - ScopedCallback::consume( $scopedOverride ); } public function testResetNotificationTimestamp_futureNotificationTimestampForced() { @@ -2436,26 +2493,28 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - $addUpdateCallCounter = 0; - $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback( - function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) { - $addUpdateCallCounter++; - $this->verifyCallbackJob( - $callable, - $title, - $user->getId(), - function ( $time ) { - return $time === '30151212010101'; - } - ); - } - ); + $mockQueueGroup->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( + function ( ActivityUpdateJob $job ) use ( $title, $user ) { + $this->verifyCallbackJob( + $job, + $title, + $user->getId(), + function ( $time ) { + return $time === '30151212010101'; + } + ); + } + ) ); $getTimestampCallCounter = 0; $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( @@ -2474,10 +2533,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 1, $addUpdateCallCounter ); - $this->assertEquals( 1, $getTimestampCallCounter ); + $this->assertEquals( 2, $getTimestampCallCounter ); - ScopedCallback::consume( $scopedOverrideDeferred ); ScopedCallback::consume( $scopedOverrideRevision ); } @@ -2515,26 +2572,28 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); + $mockQueueGroup = $this->getMockJobQueueGroup(); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $mockQueueGroup, $mockCache, $this->getMockReadOnlyMode() ); - $addUpdateCallCounter = 0; - $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback( - function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) { - $addUpdateCallCounter++; - $this->verifyCallbackJob( - $callable, - $title, - $user->getId(), - function ( $time ) { - return $time === false; - } - ); - } - ); + $mockQueueGroup->expects( $this->any() ) + ->method( 'lazyPush' ) + ->will( $this->returnCallback( + function ( ActivityUpdateJob $job ) use ( $title, $user ) { + $this->verifyCallbackJob( + $job, + $title, + $user->getId(), + function ( $time ) { + return $time === false; + } + ); + } + ) ); $getTimestampCallCounter = 0; $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( @@ -2553,16 +2612,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 1, $addUpdateCallCounter ); - $this->assertEquals( 1, $getTimestampCallCounter ); + $this->assertEquals( 2, $getTimestampCallCounter ); - ScopedCallback::consume( $scopedOverrideDeferred ); ScopedCallback::consume( $scopedOverrideRevision ); } public function testSetNotificationTimestampsForUser_anonUser() { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $this->getMockDb() ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode() ); @@ -2573,57 +2631,46 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $user = $this->getMockNonAnonUserWithId( 1 ); $timestamp = '20100101010101'; - $mockDb = $this->getMockDb(); - $mockDb->expects( $this->once() ) - ->method( 'update' ) - ->with( - 'watchlist', - [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ], - [ 'wl_user' => 1 ] - ) - ->will( $this->returnValue( true ) ); - $mockDb->expects( $this->exactly( 1 ) ) - ->method( 'timestamp' ) - ->will( $this->returnCallback( function ( $value ) { - return 'TS' . $value . 'TS'; - } ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), + $this->getMockLBFactory( $this->getMockDb() ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode() ); + // Note: This does not actually assert the job is correct + $callableCallCounter = 0; + $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { + $callableCallCounter++; + $this->assertInternalType( 'callable', $callable ); + }; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); + $this->assertTrue( $store->setNotificationTimestampsForUser( $user, $timestamp ) ); + $this->assertEquals( 1, $callableCallCounter ); } public function testSetNotificationTimestampsForUser_nullTimestamp() { $user = $this->getMockNonAnonUserWithId( 1 ); $timestamp = null; - $mockDb = $this->getMockDb(); - $mockDb->expects( $this->once() ) - ->method( 'update' ) - ->with( - 'watchlist', - [ 'wl_notificationtimestamp' => null ], - [ 'wl_user' => 1 ] - ) - ->will( $this->returnValue( true ) ); - $mockDb->expects( $this->exactly( 0 ) ) - ->method( 'timestamp' ) - ->will( $this->returnCallback( function ( $value ) { - return 'TS' . $value . 'TS'; - } ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), + $this->getMockLBFactory( $this->getMockDb() ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode() ); + // Note: This does not actually assert the job is correct + $callableCallCounter = 0; + $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { + $callableCallCounter++; + $this->assertInternalType( 'callable', $callable ); + }; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); + $this->assertTrue( $store->setNotificationTimestampsForUser( $user, $timestamp ) ); @@ -2640,7 +2687,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( 'watchlist', [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ], - [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ] + [ 'wl_user' => 1, 'wl_namespace' => 0, 'wl_title' => [ 'Foo', 'Bar' ] ] ) ->will( $this->returnValue( true ) ); $mockDb->expects( $this->exactly( 1 ) ) @@ -2649,16 +2696,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { return 'TS' . $value . 'TS'; } ) ); $mockDb->expects( $this->once() ) - ->method( 'makeWhereFrom2d' ) - ->with( - [ [ 'Foo' => 1, 'Bar' => 1 ] ], - $this->isType( 'string' ), - $this->isType( 'string' ) - ) - ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + ->method( 'affectedRows' ) + ->will( $this->returnValue( 2 ) ); $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $this->getMockCache(), $this->getMockReadOnlyMode() ); @@ -2702,6 +2745,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -2743,6 +2787,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); @@ -2787,6 +2832,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $store = $this->newWatchedItemStore( $this->getMockLBFactory( $mockDb ), + $this->getMockJobQueueGroup(), $mockCache, $this->getMockReadOnlyMode() ); diff --git a/tests/phpunit/maintenance/DumpAsserter.php b/tests/phpunit/maintenance/DumpAsserter.php new file mode 100644 index 0000000000..5b4c6efd50 --- /dev/null +++ b/tests/phpunit/maintenance/DumpAsserter.php @@ -0,0 +1,347 @@ +schemaVersion = $schemaVersion; + } + + /** + * Step the current XML reader until node end of given name is found. + * + * @param string $name Name of the closing element to look for + * (e.g.: "mediawiki" when looking for ) + * + * @return bool True if the end node could be found. false otherwise. + */ + public function skipToNodeEnd( $name ) { + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::END_ELEMENT && + $this->xml->name == $name + ) { + return true; + } + } + + return false; + } + + /** + * Step the current XML reader to the first element start after the node + * end of a given name. + * + * @param string $name Name of the closing element to look for + * (e.g.: "mediawiki" when looking for ) + * + * @return bool True if new element after the closing of $name could be + * found. false otherwise. + */ + public function skipPastNodeEnd( $name ) { + Assert::assertTrue( $this->skipToNodeEnd( $name ), + "Skipping to end of $name" ); + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::ELEMENT ) { + return true; + } + } + + return false; + } + + /** + * Opens an XML file to analyze and optionally skips past siteinfo. + * + * @param string $fname Name of file to analyze + * @param bool $skip_siteinfo (optional) If true, step the xml reader + * to the first element after + */ + public function assertDumpStart( $fname, $skip_siteinfo = true ) { + $this->xml = new XMLReader(); + + Assert::assertTrue( $this->xml->open( $fname ), + "Opening temporary file $fname via XMLReader failed" ); + if ( $skip_siteinfo ) { + Assert::assertTrue( $this->skipPastNodeEnd( "siteinfo" ), + "Skipping past end of siteinfo" ); + } + } + + /** + * Asserts that the xml reader is at the final closing tag of an xml file and + * closes the reader. + * + * @param string $name (optional) the name of the final tag + * (e.g.: "mediawiki" for ) + */ + public function assertDumpEnd( $name = "mediawiki" ) { + $this->assertNodeEnd( $name, false ); + if ( $this->xml->read() ) { + $this->skipWhitespace(); + } + Assert::assertEquals( $this->xml->nodeType, XMLReader::NONE, + "No proper entity left to parse" ); + $this->xml->close(); + } + + /** + * Steps the xml reader over white space + */ + public function skipWhitespace() { + $cont = true; + while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE ) + || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { + $cont = $this->xml->read(); + } + } + + /** + * Asserts that the xml reader is at an element of given name, and optionally + * skips past it. + * + * @param string $name The name of the element to check for + * (e.g.: "mediawiki" for ) + * @param bool $skip (optional) if true, skip past the found element + */ + public function assertNodeStart( $name, $skip = true ) { + Assert::assertEquals( $name, $this->xml->name, "Node name" ); + Assert::assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + Assert::assertTrue( $this->xml->read(), "Skipping past start tag" ); + } + } + + /** + * Asserts that the xml reader is at an closing element of given name, and optionally + * skips past it. + * + * @param string $name The name of the closing element to check for + * (e.g.: "mediawiki" for ) + * @param bool $skip (optional) if true, skip past the found element + */ + public function assertNodeEnd( $name, $skip = true ) { + Assert::assertEquals( $name, $this->xml->name, "Node name" ); + Assert::assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + Assert::assertTrue( $this->xml->read(), "Skipping past end tag" ); + } + } + + /** + * Asserts that the xml reader is at an element of given tag that contains a given text, + * and skips over the element. + * + * @param string $name The name of the element to check for + * (e.g.: "mediawiki" for ...) + * @param string|bool $text If string, check if it equals the elements text. + * If false, ignore the element's text + * @param bool $skip_ws (optional) if true, skip past white spaces that trail the + * closing element. + */ + public function assertTextNode( $name, $text, $skip_ws = true ) { + $this->assertNodeStart( $name ); + + if ( $text !== false ) { + Assert::assertEquals( $text, $this->xml->value, "Text of node " . $name ); + } + Assert::assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); + $this->assertNodeEnd( $name ); + + if ( $skip_ws ) { + $this->skipWhitespace(); + } + } + + /** + * Asserts that the xml reader is at the start of a page element and skips over the first + * tags, after checking them. + * + * Besides the opening page element, this function also checks for and skips over the + * title, ns, and id tags. Hence after this function, the xml reader is at the first + * revision of the current page. + * + * @param int $id Id of the page to assert + * @param int $ns Number of namespage to assert + * @param string $name Title of the current page + */ + public function assertPageStart( $id, $ns, $name ) { + $this->assertNodeStart( "page" ); + $this->skipWhitespace(); + + $this->assertTextNode( "title", $name ); + $this->assertTextNode( "ns", $ns ); + $this->assertTextNode( "id", $id ); + } + + /** + * Asserts that the xml reader is at the page's closing element and skips to the next + * element. + */ + public function assertPageEnd() { + $this->assertNodeEnd( "page" ); + $this->skipWhitespace(); + } + + /** + * Asserts that the xml reader is at a revision and checks its representation before + * skipping over it. + * + * @param int $id Id of the revision + * @param string $summary Summary of the revision + * @param int $text_id Id of the revision's text + * @param int $text_bytes Number of bytes in the revision's text + * @param string $text_sha1 The base36 SHA-1 of the revision's text + * @param string|bool $text (optional) The revision's string, or false to check for a + * revision stub + * @param int|bool $parentid (optional) id of the parent revision + * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT) + * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT) + */ + public function assertRevision( $id, $summary, $text_id, $text_bytes, + $text_sha1, $text = false, $parentid = false, + $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT + ) { + $this->assertNodeStart( "revision" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + if ( $parentid !== false ) { + $this->assertTextNode( "parentid", $parentid ); + } + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "ip", false ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + $this->assertTextNode( "comment", $summary ); + $this->skipWhitespace(); + + $this->assertTextNode( "model", $model ); + $this->skipWhitespace(); + + $this->assertTextNode( "format", $format ); + $this->skipWhitespace(); + + if ( $this->xml->name == "text" ) { + // note: tag may occur here or at the very end. + $text_found = true; + $this->assertText( $id, $text_id, $text_bytes, $text ); + } else { + $text_found = false; + } + + $this->assertTextNode( "sha1", $text_sha1 ); + + if ( !$text_found ) { + $this->assertText( $id, $text_id, $text_bytes, $text ); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + + public function assertText( $id, $text_id, $text_bytes, $text ) { + $this->assertNodeStart( "text", false ); + if ( $text_bytes !== false ) { + Assert::assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, + "Attribute 'bytes' of revision " . $id ); + } + + if ( $text === false ) { + // Testing for a stub + Assert::assertEquals( $this->xml->getAttribute( "id" ), $text_id, + "Text id of revision " . $id ); + Assert::assertFalse( $this->xml->hasValue, "Revision has text" ); + Assert::assertTrue( $this->xml->read(), "Skipping text start tag" ); + if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) + && ( $this->xml->name == "text" ) + ) { + $this->xml->read(); + } + $this->skipWhitespace(); + } else { + // Testing for a real dump + Assert::assertTrue( $this->xml->read(), "Skipping text start tag" ); + Assert::assertEquals( $text, $this->xml->value, "Text of revision " . $id ); + Assert::assertTrue( $this->xml->read(), "Skipping past text" ); + $this->assertNodeEnd( "text" ); + $this->skipWhitespace(); + } + } + + /** + * asserts that the xml reader is at the beginning of a log entry and skips over + * it while analyzing it. + * + * @param int $id Id of the log entry + * @param string $user_name User name of the log entry's performer + * @param int $user_id User id of the log entry 's performer + * @param string|null $comment Comment of the log entry. If null, the comment text is ignored. + * @param string $type Type of the log entry + * @param string $subtype Subtype of the log entry + * @param string $title Title of the log entry's target + * @param array $parameters (optional) unserialized data accompanying the log entry + */ + public function assertLogItem( $id, $user_name, $user_id, $comment, $type, + $subtype, $title, $parameters = [] + ) { + $this->assertNodeStart( "logitem" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "username", $user_name ); + $this->assertTextNode( "id", $user_id ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + if ( $comment !== null ) { + $this->assertTextNode( "comment", $comment ); + } + $this->assertTextNode( "type", $type ); + $this->assertTextNode( "action", $subtype ); + $this->assertTextNode( "logtitle", $title ); + + $this->assertNodeStart( "params" ); + $parameters_xml = unserialize( $this->xml->value ); + Assert::assertEquals( $parameters, $parameters_xml ); + Assert::assertTrue( $this->xml->read(), "Skipping past processed text of params" ); + $this->assertNodeEnd( "params" ); + $this->skipWhitespace(); + + $this->assertNodeEnd( "logitem" ); + $this->skipWhitespace(); + } +} diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php index 4b7a7eb3d4..26c9b92dbc 100644 --- a/tests/phpunit/maintenance/DumpTestCase.php +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -3,11 +3,12 @@ namespace MediaWiki\Tests\Maintenance; use ContentHandler; +use DOMDocument; use ExecutableFinder; use MediaWikiLangTestCase; -use Page; use User; -use XMLReader; +use WikiExporter; +use WikiPage; use MWException; /** @@ -28,13 +29,6 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { */ protected $exceptionFromAddDBData = null; - /** - * Holds the XMLReader used for analyzing an XML dump - * - * @var XMLReader|null - */ - protected $xml = null; - /** @var bool|null Whether the 'gzip' utility is available */ protected static $hasGzip = null; @@ -58,7 +52,7 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { /** * Adds a revision to a page, while returning the resuting revision's id * - * @param Page $page Page to add the revision to + * @param WikiPage $page Page to add the revision to * @param string $text Revisions text * @param string $summary Revisions summary * @param string $model The model ID (defaults to wikitext) @@ -66,7 +60,12 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { * @throws MWException * @return array */ - protected function addRevision( Page $page, $text, $summary, $model = CONTENT_MODEL_WIKITEXT ) { + protected function addRevision( + WikiPage $page, + $text, + $summary, + $model = CONTENT_MODEL_WIKITEXT + ) { $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), $model ), $summary @@ -108,6 +107,36 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { ); } + public static function setUpBeforeClass() { + parent::setUpBeforeClass(); + + if ( !function_exists( 'libxml_set_external_entity_loader' ) ) { + return; + } + + // The W3C is intentionally slow about returning schema files, + // see . + // To work around that, we keep our own copies of the relevant schema files. + libxml_set_external_entity_loader( + function ( $public, $system, $context ) { + switch ( $system ) { + // if more schema files are needed, add them here. + case 'http://www.w3.org/2001/xml.xsd': + $file = __DIR__ . '/xml.xsd'; + break; + default: + if ( is_file( $system ) ) { + $file = $system; + } else { + return null; + } + } + + return $file; + } + ); + } + /** * Default set up function. * @@ -125,6 +154,21 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { $this->setMwGlobals( 'wgUser', new User() ); } + /** + * Returns the path to the XML schema file for the given schema version. + * + * @param string|null $schemaVersion + * + * @return string + */ + protected function getXmlSchemaPath( $schemaVersion = null ) { + global $IP, $wgXmlDumpSchemaVersion; + + $schemaVersion = $schemaVersion ?: $wgXmlDumpSchemaVersion; + + return "$IP/docs/export-$schemaVersion.xsd"; + } + /** * Checks for test output consisting only of lines containing ETA announcements */ @@ -152,266 +196,62 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { } /** - * Step the current XML reader until node end of given name is found. - * - * @param string $name Name of the closing element to look for - * (e.g.: "mediawiki" when looking for ) + * @param null|string $schemaVersion * - * @return bool True if the end node could be found. false otherwise. + * @return DumpAsserter */ - protected function skipToNodeEnd( $name ) { - while ( $this->xml->read() ) { - if ( $this->xml->nodeType == XMLReader::END_ELEMENT && - $this->xml->name == $name - ) { - return true; - } - } - - return false; + protected function getDumpAsserter( $schemaVersion = null ) { + $schemaVersion = $schemaVersion ?: WikiExporter::schemaVersion(); + return new DumpAsserter( $schemaVersion ); } /** - * Step the current XML reader to the first element start after the node - * end of a given name. - * - * @param string $name Name of the closing element to look for - * (e.g.: "mediawiki" when looking for ) - * - * @return bool True if new element after the closing of $name could be - * found. false otherwise. + * Checks an XML file against an XSD schema. */ - protected function skipPastNodeEnd( $name ) { - $this->assertTrue( $this->skipToNodeEnd( $name ), - "Skipping to end of $name" ); - while ( $this->xml->read() ) { - if ( $this->xml->nodeType == XMLReader::ELEMENT ) { - return true; - } + protected function assertDumpSchema( $fname, $schemaFile ) { + if ( !function_exists( 'libxml_use_internal_errors' ) ) { + // Would be nice to leave a warning somehow. + // We don't want to skip all of the test case that calls this, though. + $this->markAsRisky(); + return; } - - return false; - } - - /** - * Opens an XML file to analyze and optionally skips past siteinfo. - * - * @param string $fname Name of file to analyze - * @param bool $skip_siteinfo (optional) If true, step the xml reader - * to the first element after - */ - protected function assertDumpStart( $fname, $skip_siteinfo = true ) { - $this->xml = new XMLReader(); - $this->assertTrue( $this->xml->open( $fname ), - "Opening temporary file $fname via XMLReader failed" ); - if ( $skip_siteinfo ) { - $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ), - "Skipping past end of siteinfo" ); + if ( defined( 'HHVM_VERSION' ) ) { + // In HHVM, loading a schema from a file is disabled per default. + // This is controlled by hhvm.libxml.ext_entity_whitelist which + // cannot be read with ini_get(), see + // . + // Would be nice to leave a warning somehow. + // We don't want to skip all of the test case that calls this, though. + $this->markAsRisky(); + return; } - } - /** - * Asserts that the xml reader is at the final closing tag of an xml file and - * closes the reader. - * - * @param string $name (optional) the name of the final tag - * (e.g.: "mediawiki" for ) - */ - protected function assertDumpEnd( $name = "mediawiki" ) { - $this->assertNodeEnd( $name, false ); - if ( $this->xml->read() ) { - $this->skipWhitespace(); - } - $this->assertEquals( $this->xml->nodeType, XMLReader::NONE, - "No proper entity left to parse" ); - $this->xml->close(); - } + $xml = new DOMDocument(); + $this->assertTrue( $xml->load( $fname ), + "Opening temporary file $fname via DOMDocument failed" ); - /** - * Steps the xml reader over white space - */ - protected function skipWhitespace() { - $cont = true; - while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE ) - || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { - $cont = $this->xml->read(); - } - } + // Don't throw + $oldLibXmlInternalErrors = libxml_use_internal_errors( true ); - /** - * Asserts that the xml reader is at an element of given name, and optionally - * skips past it. - * - * @param string $name The name of the element to check for - * (e.g.: "mediawiki" for ) - * @param bool $skip (optional) if true, skip past the found element - */ - protected function assertNodeStart( $name, $skip = true ) { - $this->assertEquals( $name, $this->xml->name, "Node name" ); - $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); - if ( $skip ) { - $this->assertTrue( $this->xml->read(), "Skipping past start tag" ); - } - } - - /** - * Asserts that the xml reader is at an closing element of given name, and optionally - * skips past it. - * - * @param string $name The name of the closing element to check for - * (e.g.: "mediawiki" for ) - * @param bool $skip (optional) if true, skip past the found element - */ - protected function assertNodeEnd( $name, $skip = true ) { - $this->assertEquals( $name, $this->xml->name, "Node name" ); - $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); - if ( $skip ) { - $this->assertTrue( $this->xml->read(), "Skipping past end tag" ); - } - } - - /** - * Asserts that the xml reader is at an element of given tag that contains a given text, - * and skips over the element. - * - * @param string $name The name of the element to check for - * (e.g.: "mediawiki" for ...) - * @param string|bool $text If string, check if it equals the elements text. - * If false, ignore the element's text - * @param bool $skip_ws (optional) if true, skip past white spaces that trail the - * closing element. - */ - protected function assertTextNode( $name, $text, $skip_ws = true ) { - $this->assertNodeStart( $name ); - - if ( $text !== false ) { - $this->assertEquals( $text, $this->xml->value, "Text of node " . $name ); - } - $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); - $this->assertNodeEnd( $name ); - - if ( $skip_ws ) { - $this->skipWhitespace(); - } - } - - /** - * Asserts that the xml reader is at the start of a page element and skips over the first - * tags, after checking them. - * - * Besides the opening page element, this function also checks for and skips over the - * title, ns, and id tags. Hence after this function, the xml reader is at the first - * revision of the current page. - * - * @param int $id Id of the page to assert - * @param int $ns Number of namespage to assert - * @param string $name Title of the current page - */ - protected function assertPageStart( $id, $ns, $name ) { - $this->assertNodeStart( "page" ); - $this->skipWhitespace(); - - $this->assertTextNode( "title", $name ); - $this->assertTextNode( "ns", $ns ); - $this->assertTextNode( "id", $id ); - } + // NOTE: if this reports "Invalid Schema", the schema may be referencing an external + // entity (typically, another schema) that needs to be mapped in the + // libxml_set_external_entity_loader callback defined in setUpBeforeClass() above! + // Or $schemaFile doesn't point to a schema file, or the schema is indeed just broken. + if ( !$xml->schemaValidate( $schemaFile ) ) { + $errorText = ''; - /** - * Asserts that the xml reader is at the page's closing element and skips to the next - * element. - */ - protected function assertPageEnd() { - $this->assertNodeEnd( "page" ); - $this->skipWhitespace(); - } - - /** - * Asserts that the xml reader is at a revision and checks its representation before - * skipping over it. - * - * @param int $id Id of the revision - * @param string $summary Summary of the revision - * @param int $text_id Id of the revision's text - * @param int $text_bytes Number of bytes in the revision's text - * @param string $text_sha1 The base36 SHA-1 of the revision's text - * @param string|bool $text (optional) The revision's string, or false to check for a - * revision stub - * @param int|bool $parentid (optional) id of the parent revision - * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT) - * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT) - */ - protected function assertRevision( $id, $summary, $text_id, $text_bytes, - $text_sha1, $text = false, $parentid = false, - $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT - ) { - $this->assertNodeStart( "revision" ); - $this->skipWhitespace(); - - $this->assertTextNode( "id", $id ); - if ( $parentid !== false ) { - $this->assertTextNode( "parentid", $parentid ); - } - $this->assertTextNode( "timestamp", false ); - - $this->assertNodeStart( "contributor" ); - $this->skipWhitespace(); - $this->assertTextNode( "ip", false ); - $this->assertNodeEnd( "contributor" ); - $this->skipWhitespace(); - - $this->assertTextNode( "comment", $summary ); - $this->skipWhitespace(); - - $this->assertTextNode( "model", $model ); - $this->skipWhitespace(); - - $this->assertTextNode( "format", $format ); - $this->skipWhitespace(); - - if ( $this->xml->name == "text" ) { - // note: tag may occur here or at the very end. - $text_found = true; - $this->assertText( $id, $text_id, $text_bytes, $text ); - } else { - $text_found = false; - } + foreach ( libxml_get_errors() as $error ) { + $errorText .= "\nline {$error->line}: {$error->message}"; + } - $this->assertTextNode( "sha1", $text_sha1 ); + libxml_clear_errors(); - if ( !$text_found ) { - $this->assertText( $id, $text_id, $text_bytes, $text ); + $this->fail( + "Failed asserting that $fname conforms to the schema in $schemaFile:\n$errorText" + ); } - $this->assertNodeEnd( "revision" ); - $this->skipWhitespace(); + libxml_use_internal_errors( $oldLibXmlInternalErrors ); } - protected function assertText( $id, $text_id, $text_bytes, $text ) { - $this->assertNodeStart( "text", false ); - if ( $text_bytes !== false ) { - $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, - "Attribute 'bytes' of revision " . $id ); - } - - if ( $text === false ) { - // Testing for a stub - $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id, - "Text id of revision " . $id ); - $this->assertFalse( $this->xml->hasValue, "Revision has text" ); - $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); - if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) - && ( $this->xml->name == "text" ) - ) { - $this->xml->read(); - } - $this->skipWhitespace(); - } else { - // Testing for a real dump - $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); - $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id ); - $this->assertTrue( $this->xml->read(), "Skipping past text" ); - $this->assertNodeEnd( "text" ); - $this->skipWhitespace(); - } - } } diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php index 8824c7af58..747d2bfa13 100644 --- a/tests/phpunit/maintenance/backupPrefetchTest.php +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -6,8 +6,6 @@ use BaseDump; use MediaWikiTestCase; /** - * Tests for BaseDump - * * @group Dump * @covers BaseDump */ diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php index b8a60bec39..0d4bc56cb0 100644 --- a/tests/phpunit/maintenance/backupTextPassTest.php +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -11,8 +11,6 @@ use Title; use WikiExporter; use WikiPage; -require_once __DIR__ . "/../../../maintenance/dumpTextPass.php"; - /** * Tests for TextPassDumper that rely on the database * @@ -132,45 +130,46 @@ class TextPassDumperDatabaseTest extends DumpTestCase { $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); // Checking for correctness of the dumped data - $this->assertDumpStart( $nameFull ); + $asserter = $this->getDumpAsserter(); + $asserter->assertDumpStart( $nameFull ); // Page 1 - $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $asserter->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $asserter->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", "BackupDumperTestP1Text1" ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); - $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $asserter->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $asserter->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", "BackupDumperTestP2Text1" ); - $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $asserter->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", "BackupDumperTestP2Text2", $this->revId2_1 ); - $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $asserter->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", "BackupDumperTestP2Text3", $this->revId2_2 ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $asserter->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $asserter->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1", false, "BackupTextPassTestModel", "text/plain" ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); } function testPrefetchPlain() { @@ -204,49 +203,50 @@ class TextPassDumperDatabaseTest extends DumpTestCase { $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); // Checking for correctness of the dumped data - $this->assertDumpStart( $nameFull ); + $asserter = $this->getDumpAsserter(); + $asserter->assertDumpStart( $nameFull ); // Page 1 - $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $asserter->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); // Prefetch kicks in. This is still the SHA-1 of the original text, // But the actual text (with different SHA-1) comes from prefetch. - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $asserter->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", "Prefetch_________1Text1" ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); - $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $asserter->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $asserter->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", "BackupDumperTestP2Text1" ); - $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $asserter->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", "BackupDumperTestP2Text2", $this->revId2_1 ); // Prefetch kicks in. This is still the SHA-1 of the original text, // But the actual text (with different SHA-1) comes from prefetch. - $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $asserter->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", "Prefetch_________2Text3", $this->revId2_2 ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $asserter->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $asserter->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1", false, "BackupTextPassTestModel", "text/plain" ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); } /** @@ -331,6 +331,8 @@ class TextPassDumperDatabaseTest extends DumpTestCase { $lookingForPage = 1; $checkpointFiles = 0; + $asserter = $this->getDumpAsserter(); + // Each run of the following loop body tries to handle exactly 1 /page/ (not // iteration of stub content). $i is only increased after having treated page 4. for ( $i = 0; $i < $iterations; ) { @@ -348,7 +350,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase { if ( $checkpointFormat == "gzip" ) { $this->gunzip( $nameOutputDir . "/" . $fname ); } - $this->assertDumpStart( $nameOutputDir . "/" . $fname ); + $asserter->assertDumpStart( $nameOutputDir . "/" . $fname ); $fileOpened = true; $checkpointFiles++; } @@ -357,51 +359,90 @@ class TextPassDumperDatabaseTest extends DumpTestCase { switch ( $lookingForPage ) { case 1: // Page 1 - $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN, - "BackupDumperTestP1" ); - $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1", - $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", - "BackupDumperTestP1Text1" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1 + $i * self::$numOfPages, + NS_MAIN, + "BackupDumperTestP1" + ); + $asserter->assertRevision( + $this->revId1_1 + $i * self::$numOfRevs, + "BackupDumperTestP1Summary1", + $this->textId1_1, + false, + "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" + ); + $asserter->assertPageEnd(); $lookingForPage = 2; break; case 2: // Page 2 - $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN, - "BackupDumperTestP2" ); - $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1", - $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", - "BackupDumperTestP2Text1" ); - $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2", - $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", - "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs ); - $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3", - $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", - "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs ); - $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs, + $asserter->assertPageStart( + $this->pageId2 + $i * self::$numOfPages, + NS_MAIN, + "BackupDumperTestP2" + ); + $asserter->assertRevision( + $this->revId2_1 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary1", + $this->textId2_1, + false, + "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" + ); + $asserter->assertRevision( + $this->revId2_2 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary2", + $this->textId2_2, + false, + "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", + $this->revId2_1 + $i * self::$numOfRevs + ); + $asserter->assertRevision( + $this->revId2_3 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary3", + $this->textId2_3, + false, + "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", + $this->revId2_2 + $i * self::$numOfRevs + ); + $asserter->assertRevision( + $this->revId2_4 + $i * self::$numOfRevs, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + $this->textId2_4, + false, + "6o1ciaxa6pybnqprmungwofc4lv00wv", "BackupDumperTestP2Text4 some additional Text", - $this->revId2_3 + $i * self::$numOfRevs ); - $this->assertPageEnd(); + $this->revId2_3 + $i * self::$numOfRevs + ); + $asserter->assertPageEnd(); $lookingForPage = 4; break; case 4: // Page 4 - $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK, - "Talk:BackupDumperTestP1" ); - $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs, + $asserter->assertPageStart( + $this->pageId4 + $i * self::$numOfPages, + NS_TALK, + "Talk:BackupDumperTestP1" + ); + $asserter->assertRevision( + $this->revId4_1 + $i * self::$numOfRevs, "Talk BackupDumperTestP1 Summary1", - $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + $this->textId4_1, + false, + "nktofwzd0tl192k3zfepmlzxoax1lpe", "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1", false, "BackupTextPassTestModel", - "text/plain" ); - $this->assertPageEnd(); + "text/plain" + ); + $asserter->assertPageEnd(); $lookingForPage = 1; @@ -417,7 +458,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase { if ( $this->xml->nodeType == XMLReader::END_ELEMENT && $this->xml->name == "mediawiki" ) { - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); $fileOpened = false; } } diff --git a/tests/phpunit/maintenance/backup_LogTest.php b/tests/phpunit/maintenance/backup_LogTest.php index 9357451481..811f1ee501 100644 --- a/tests/phpunit/maintenance/backup_LogTest.php +++ b/tests/phpunit/maintenance/backup_LogTest.php @@ -2,6 +2,7 @@ namespace MediaWiki\Tests\Maintenance; +use Exception; use MediaWiki\MediaWikiServices; use DumpBackup; use ManualLogEntry; @@ -98,53 +99,6 @@ class BackupDumperLoggerTest extends DumpTestCase { } } - /** - * asserts that the xml reader is at the beginning of a log entry and skips over - * it while analyzing it. - * - * @param int $id Id of the log entry - * @param string $user_name User name of the log entry's performer - * @param int $user_id User id of the log entry 's performer - * @param string|null $comment Comment of the log entry. If null, the comment text is ignored. - * @param string $type Type of the log entry - * @param string $subtype Subtype of the log entry - * @param string $title Title of the log entry's target - * @param array $parameters (optional) unserialized data accompanying the log entry - */ - private function assertLogItem( $id, $user_name, $user_id, $comment, $type, - $subtype, $title, $parameters = [] - ) { - $this->assertNodeStart( "logitem" ); - $this->skipWhitespace(); - - $this->assertTextNode( "id", $id ); - $this->assertTextNode( "timestamp", false ); - - $this->assertNodeStart( "contributor" ); - $this->skipWhitespace(); - $this->assertTextNode( "username", $user_name ); - $this->assertTextNode( "id", $user_id ); - $this->assertNodeEnd( "contributor" ); - $this->skipWhitespace(); - - if ( $comment !== null ) { - $this->assertTextNode( "comment", $comment ); - } - $this->assertTextNode( "type", $type ); - $this->assertTextNode( "action", $subtype ); - $this->assertTextNode( "logtitle", $title ); - - $this->assertNodeStart( "params" ); - $parameters_xml = unserialize( $this->xml->value ); - $this->assertEquals( $parameters, $parameters_xml ); - $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" ); - $this->assertNodeEnd( "params" ); - $this->skipWhitespace(); - - $this->assertNodeEnd( "logitem" ); - $this->skipWhitespace(); - } - function testPlain() { // Preparing the dump $fname = $this->getNewTempFile(); @@ -159,9 +113,12 @@ class BackupDumperLoggerTest extends DumpTestCase { $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); // Analyzing the dumped data - $this->assertDumpStart( $fname ); + $this->assertDumpSchema( $fname, $this->getXmlSchemaPath() ); + + $asserter = $this->getDumpAsserter(); + $asserter->assertDumpStart( $fname ); - $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $asserter->assertLogItem( $this->logId1, "BackupDumperLogUserA", $this->userId1, null, "type", "subtype", "PageA" ); $contLang = MediaWikiServices::getInstance()->getContentLanguage(); @@ -169,15 +126,15 @@ class BackupDumperLoggerTest extends DumpTestCase { $namespace = $contLang->getNsText( NS_TALK ); $this->assertInternalType( 'string', $namespace ); $this->assertGreaterThan( 0, strlen( $namespace ) ); - $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $asserter->assertLogItem( $this->logId2, "BackupDumperLogUserB", $this->userId2, "SomeComment", "supress", "delete", $namespace . ":PageB" ); - $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $asserter->assertLogItem( $this->logId3, "BackupDumperLogUserB", $this->userId2, "SomeOtherComment", "move", "delete", "PageA", [ 'key1' => 1, 3 => 'value3' ] ); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); } function testXmlDumpsBackupUseCaseLogging() { @@ -211,9 +168,12 @@ class BackupDumperLoggerTest extends DumpTestCase { // Analyzing the dumped data $this->gunzip( $fname ); - $this->assertDumpStart( $fname ); + $this->assertDumpSchema( $fname, $this->getXmlSchemaPath() ); + + $asserter = $this->getDumpAsserter(); + $asserter->assertDumpStart( $fname ); - $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $asserter->assertLogItem( $this->logId1, "BackupDumperLogUserA", $this->userId1, null, "type", "subtype", "PageA" ); $contLang = MediaWikiServices::getInstance()->getContentLanguage(); @@ -221,15 +181,15 @@ class BackupDumperLoggerTest extends DumpTestCase { $namespace = $contLang->getNsText( NS_TALK ); $this->assertInternalType( 'string', $namespace ); $this->assertGreaterThan( 0, strlen( $namespace ) ); - $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $asserter->assertLogItem( $this->logId2, "BackupDumperLogUserB", $this->userId2, "SomeComment", "supress", "delete", $namespace . ":PageB" ); - $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $asserter->assertLogItem( $this->logId3, "BackupDumperLogUserB", $this->userId2, "SomeOtherComment", "move", "delete", "PageA", [ 'key1' => 1, 3 => 'value3' ] ); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); // Currently, no reporting is implemented. Alert via failure, once // this changes. diff --git a/tests/phpunit/maintenance/backup_PageTest.php b/tests/phpunit/maintenance/backup_PageTest.php index 000b50f1d7..17c8757b3c 100644 --- a/tests/phpunit/maintenance/backup_PageTest.php +++ b/tests/phpunit/maintenance/backup_PageTest.php @@ -3,9 +3,16 @@ namespace MediaWiki\Tests\Maintenance; use DumpBackup; +use Exception; +use MediaWiki\MediaWikiServices; +use MediaWikiTestCase; +use MWException; use Title; use WikiExporter; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\LoadBalancer; use WikiPage; +use XmlDumpWriter; /** * Tests for page dumps of BackupDumper @@ -27,6 +34,11 @@ class BackupDumperPageTest extends DumpTestCase { private $revId4_1, $textId4_1; private $namespace, $talk_namespace; + /** + * @var LoadBalancer|null + */ + private $streamingLoadBalancer = null; + function addDBData() { // be sure, titles created here using english namespace names $this->setContentLang( 'en' ); @@ -101,154 +113,359 @@ class BackupDumperPageTest extends DumpTestCase { "Page ids increasing without holes" ); } - function testFullTextPlain() { + function tearDown() { + parent::tearDown(); + + if ( isset( $this->streamingLoadBalancer ) ) { + $this->streamingLoadBalancer->closeAll(); + } + } + + /** + * Returns a new database connection which is separate from the conenctions returned + * by the default LoadBalancer instance. + * + * @return IDatabase + */ + private function newStreamingDBConnection() { + // Create a *new* LoadBalancer, so no connections are shared + if ( !$this->streamingLoadBalancer ) { + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + + $this->streamingLoadBalancer = $lbFactory->newMainLB(); + } + + $db = $this->streamingLoadBalancer->getConnection( DB_REPLICA ); + + // Make sure the DB connection has the fake table clones and the fake table prefix + MediaWikiTestCase::setupDatabaseWithTestPrefix( $db ); + + // Make sure the DB connection has all the test data + $this->copyTestData( $this->db, $db ); + + return $db; + } + + /** + * @param array $argv + * @param int $startId + * @param int $endId + * + * @return DumpBackup + */ + private function newDumpBackup( $argv, $startId, $endId ) { + $dumper = new DumpBackup( $argv ); + $dumper->startId = $startId; + $dumper->endId = $endId; + $dumper->reporting = false; + + // NOTE: The copyTestData() method used by newStreamingDBConnection() + // doesn't work with SQLite (T217607). + // But DatabaseSqlite doesn't support streaming anyway, so just skip that part. + if ( $this->db->getType() === 'sqlite' ) { + $dumper->setDB( $this->db ); + } else { + $dumper->setDB( $this->newStreamingDBConnection() ); + } + + return $dumper; + } + + public function schemaVersionProvider() { + foreach ( XmlDumpWriter::$supportedSchemas as $schemaVersion ) { + yield [ $schemaVersion ]; + } + } + + /** + * @dataProvider schemaVersionProvider + */ + function testFullTextPlain( $schemaVersion ) { // Preparing the dump $fname = $this->getNewTempFile(); - $dumper = new DumpBackup(); - $dumper->loadWithArgv( [ '--full', '--quiet', '--output', 'file:' . $fname ] ); - $dumper->startId = $this->pageId1; - $dumper->endId = $this->pageId4 + 1; - $dumper->setDB( $this->db ); + $dumper = $this->newDumpBackup( + [ '--full', '--quiet', '--output', 'file:' . $fname, '--schema-version', $schemaVersion ], + $this->pageId1, + $this->pageId4 + 1 + ); // Performing the dump $dumper->execute(); // Checking the dumped data - $this->assertDumpStart( $fname ); + $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) ); + $asserter = $this->getDumpAsserter( $schemaVersion ); + + $asserter->assertDumpStart( $fname ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87", - "BackupDumperTestP1Text1" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", - $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2", - "BackupDumperTestP2Text1" ); - $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", - $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", - "BackupDumperTestP2Text2", $this->revId2_1 ); - $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", - $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", - "BackupDumperTestP2Text3", $this->revId2_2 ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", - "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_1, + "BackupDumperTestP2Summary1", + $this->textId2_1, + 23, + "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" + ); + $asserter->assertRevision( + $this->revId2_2, + "BackupDumperTestP2Summary2", + $this->textId2_2, + 23, + "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", + $this->revId2_1 + ); + $asserter->assertRevision( + $this->revId2_3, + "BackupDumperTestP2Summary3", + $this->textId2_3, + 23, + "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", + $this->revId2_2 + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( + $asserter->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", - $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe", - "Talk about BackupDumperTestP1 Text1" ); - $this->assertPageEnd(); + $asserter->assertRevision( + $this->revId4_1, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, + 35, + "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1", + false, + CONTENT_MODEL_WIKITEXT, + CONTENT_FORMAT_WIKITEXT, + $schemaVersion + ); + $asserter->assertPageEnd(); + + $asserter->assertDumpEnd(); - $this->assertDumpEnd(); + // FIXME: add multi-slot test case! } - function testFullStubPlain() { + /** + * @dataProvider schemaVersionProvider + */ + function testFullStubPlain( $schemaVersion ) { // Preparing the dump $fname = $this->getNewTempFile(); - $dumper = new DumpBackup(); - $dumper->loadWithArgv( [ '--full', '--quiet', '--output', 'file:' . $fname, '--stub' ] ); - $dumper->startId = $this->pageId1; - $dumper->endId = $this->pageId4 + 1; - $dumper->setDB( $this->db ); + $dumper = $this->newDumpBackup( + [ + '--full', + '--quiet', + '--output', + 'file:' . $fname, + '--stub', + '--schema-version', $schemaVersion, + ], + $this->pageId1, + $this->pageId4 + 1 + ); // Performing the dump $dumper->execute(); // Checking the dumped data - $this->assertDumpStart( $fname ); + $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) ); + $asserter = $this->getDumpAsserter( $schemaVersion ); + + $asserter->assertDumpStart( $fname ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", - $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); - $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", - $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); - $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", - $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_1, + "BackupDumperTestP2Summary1", + $this->textId2_1, + 23, + "jprywrymfhysqllua29tj3sc7z39dl2" + ); + $asserter->assertRevision( + $this->revId2_2, + "BackupDumperTestP2Summary2", + $this->textId2_2, + 23, + "b7vj5ks32po5m1z1t1br4o7scdwwy95", + false, + $this->revId2_1 + ); + $asserter->assertRevision( + $this->revId2_3, + "BackupDumperTestP2Summary3", + $this->textId2_3, + 23, + "jfunqmh1ssfb8rs43r19w98k28gg56r", + false, + $this->revId2_2 + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + false, + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( + $asserter->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", - $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); - $this->assertPageEnd(); + $asserter->assertRevision( + $this->revId4_1, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, + 35, + "nktofwzd0tl192k3zfepmlzxoax1lpe" + ); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); } - function testCurrentStubPlain() { + /** + * @dataProvider schemaVersionProvider + */ + function testCurrentStubPlain( $schemaVersion ) { // Preparing the dump $fname = $this->getNewTempFile(); - $dumper = new DumpBackup( [ '--output', 'file:' . $fname ] ); - $dumper->startId = $this->pageId1; - $dumper->endId = $this->pageId4 + 1; - $dumper->reporting = false; - $dumper->setDB( $this->db ); + $dumper = $this->newDumpBackup( + [ '--output', 'file:' . $fname, '--schema-version', $schemaVersion ], + $this->pageId1, + $this->pageId4 + 1 + ); // Performing the dump $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); // Checking the dumped data - $this->assertDumpStart( $fname ); + $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) ); + + $asserter = $this->getDumpAsserter( $schemaVersion ); + $asserter->assertDumpStart( $fname ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + false, + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( + $asserter->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", - $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); - $this->assertPageEnd(); + $asserter->assertRevision( + $this->revId4_1, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, + 35, + "nktofwzd0tl192k3zfepmlzxoax1lpe" + ); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); } function testCurrentStubGzip() { @@ -257,45 +474,67 @@ class BackupDumperPageTest extends DumpTestCase { // Preparing the dump $fname = $this->getNewTempFile(); - $dumper = new DumpBackup( [ '--output', 'gzip:' . $fname ] ); - $dumper->startId = $this->pageId1; - $dumper->endId = $this->pageId4 + 1; - $dumper->reporting = false; - $dumper->setDB( $this->db ); + $dumper = $this->newDumpBackup( + [ '--output', 'gzip:' . $fname ], + $this->pageId1, + $this->pageId4 + 1 + ); // Performing the dump $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); // Checking the dumped data $this->gunzip( $fname ); - $this->assertDumpStart( $fname ); + + $asserter = $this->getDumpAsserter(); + $asserter->assertDumpStart( $fname ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + false, + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( + $asserter->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); - $this->assertPageEnd(); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); } /** @@ -308,22 +547,27 @@ class BackupDumperPageTest extends DumpTestCase { * * We reproduce such a setup with our mini fixture, although we omit * chunks, and all the other gimmicks of xmldumps-backup. + * + * @dataProvider schemaVersionProvider */ - function testXmlDumpsBackupUseCase() { + function testXmlDumpsBackupUseCase( $schemaVersion ) { $this->checkHasGzip(); $fnameMetaHistory = $this->getNewTempFile(); $fnameMetaCurrent = $this->getNewTempFile(); $fnameArticles = $this->getNewTempFile(); - $dumper = new DumpBackup( [ "--full", "--stub", "--output=gzip:" . $fnameMetaHistory, - "--output=gzip:" . $fnameMetaCurrent, "--filter=latest", - "--output=gzip:" . $fnameArticles, "--filter=latest", - "--filter=notalk", "--filter=namespace:!NS_USER", - "--reporting=1000" ] ); - $dumper->startId = $this->pageId1; - $dumper->endId = $this->pageId4 + 1; - $dumper->setDB( $this->db ); + $dumper = $this->newDumpBackup( + [ "--full", "--stub", "--output=gzip:" . $fnameMetaHistory, + "--output=gzip:" . $fnameMetaCurrent, "--filter=latest", + "--output=gzip:" . $fnameArticles, "--filter=latest", + "--filter=notalk", "--filter=namespace:!NS_USER", + "--reporting=1000", '--schema-version', $schemaVersion + ], + $this->pageId1, + $this->pageId4 + 1 + ); + $dumper->reporting = true; // xmldumps-backup uses reporting. We will not check the exact reported // message, as they are dependent on the processing power of the used @@ -342,89 +586,187 @@ class BackupDumperPageTest extends DumpTestCase { // Checking meta-history ------------------------------------------------- $this->gunzip( $fnameMetaHistory ); - $this->assertDumpStart( $fnameMetaHistory ); + $this->assertDumpSchema( $fnameMetaHistory, $this->getXmlSchemaPath( $schemaVersion ) ); + + $asserter = $this->getDumpAsserter( $schemaVersion ); + $asserter->assertDumpStart( $fnameMetaHistory ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", - $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); - $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", - $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); - $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", - $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_1, + "BackupDumperTestP2Summary1", + $this->textId2_1, + 23, + "jprywrymfhysqllua29tj3sc7z39dl2" + ); + $asserter->assertRevision( + $this->revId2_2, + "BackupDumperTestP2Summary2", + $this->textId2_2, + 23, + "b7vj5ks32po5m1z1t1br4o7scdwwy95", + false, + $this->revId2_1 + ); + $asserter->assertRevision( + $this->revId2_3, + "BackupDumperTestP2Summary3", + $this->textId2_3, + 23, + "jfunqmh1ssfb8rs43r19w98k28gg56r", + false, + $this->revId2_2 + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + false, + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( + $asserter->assertPageStart( $this->pageId4, $this->talk_namespace, - $this->pageTitle4->getPrefixedText() + $this->pageTitle4->getPrefixedText( $schemaVersion ) ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", - $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); - $this->assertPageEnd(); + $asserter->assertRevision( + $this->revId4_1, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, + 35, + "nktofwzd0tl192k3zfepmlzxoax1lpe" + ); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); // Checking meta-current ------------------------------------------------- $this->gunzip( $fnameMetaCurrent ); - $this->assertDumpStart( $fnameMetaCurrent ); + $this->assertDumpSchema( $fnameMetaCurrent, $this->getXmlSchemaPath( $schemaVersion ) ); + + $asserter = $this->getDumpAsserter( $schemaVersion ); + $asserter->assertDumpStart( $fnameMetaCurrent ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + false, + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible // Page 4 - $this->assertPageStart( + $asserter->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); - $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", - $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); - $this->assertPageEnd(); + $asserter->assertRevision( + $this->revId4_1, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, + 35, + "nktofwzd0tl192k3zfepmlzxoax1lpe" + ); + $asserter->assertPageEnd(); - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); // Checking articles ------------------------------------------------- $this->gunzip( $fnameArticles ); - $this->assertDumpStart( $fnameArticles ); + $this->assertDumpSchema( $fnameArticles, $this->getXmlSchemaPath( $schemaVersion ) ); + + $asserter = $this->getDumpAsserter( $schemaVersion ); + $asserter->assertDumpStart( $fnameArticles ); // Page 1 - $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); - $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", - $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId1, + $this->namespace, + $this->pageTitle1->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId1_1, + "BackupDumperTestP1Summary1", + $this->textId1_1, + 23, + "0bolhl6ol7i6x0e7yq91gxgaan39j87" + ); + $asserter->assertPageEnd(); // Page 2 - $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); - $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", - $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); - $this->assertPageEnd(); + $asserter->assertPageStart( + $this->pageId2, + $this->namespace, + $this->pageTitle2->getPrefixedText() + ); + $asserter->assertRevision( + $this->revId2_4, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, + 44, + "6o1ciaxa6pybnqprmungwofc4lv00wv", + false, + $this->revId2_3 + ); + $asserter->assertPageEnd(); // Page 3 // -> Page is marked deleted. Hence not visible @@ -432,7 +774,7 @@ class BackupDumperPageTest extends DumpTestCase { // Page 4 // -> Page is not in $this->namespace. Hence not visible - $this->assertDumpEnd(); + $asserter->assertDumpEnd(); $this->expectETAOutput(); } diff --git a/tests/phpunit/maintenance/categoryChangesAsRdfTest.php b/tests/phpunit/maintenance/categoryChangesAsRdfTest.php new file mode 100644 index 0000000000..521705e16e --- /dev/null +++ b/tests/phpunit/maintenance/categoryChangesAsRdfTest.php @@ -0,0 +1,265 @@ +setMwGlobals( [ + 'wgServer' => 'http://acme.test', + 'wgCanonicalServer' => 'http://acme.test', + 'wgArticlePath' => '/wiki/$1', + ] ); + } + + public function provideCategoryData() { + return [ + 'delete category' => [ + __DIR__ . "/../data/categoriesrdf/delete.sparql", + 'getDeletedCatsIterator', + 'handleDeletes', + [ + (object)[ 'rc_title' => 'Test', 'rc_cur_id' => 1, '_processed' => 1 ], + (object)[ 'rc_title' => 'Test 2', 'rc_cur_id' => 2, '_processed' => 2 ], + ], + ], + 'move category' => [ + __DIR__ . "/../data/categoriesrdf/move.sparql", + 'getMovedCatsIterator', + 'handleMoves', + [ + (object)[ + 'rc_title' => 'Test', + 'rc_cur_id' => 4, + 'page_title' => 'MovedTo', + 'page_namespace' => NS_CATEGORY, + '_processed' => 4, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'MovedTo', + 'rc_cur_id' => 4, + 'page_title' => 'MovedAgain', + 'page_namespace' => NS_CATEGORY, + 'pp_propname' => 'hiddencat', + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Test 2', + 'rc_cur_id' => 5, + 'page_title' => 'AlsoMoved', + 'page_namespace' => NS_CATEGORY, + '_processed' => 5, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Test 3', + 'rc_cur_id' => 6, + 'page_title' => 'MovedOut', + 'page_namespace' => NS_MAIN, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Test 4', + 'rc_cur_id' => 7, + 'page_title' => 'Already Done', + 'page_namespace' => NS_CATEGORY, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + ], + [ 7 => true ], + ], + 'restore deleted category' => [ + __DIR__ . "/../data/categoriesrdf/restore.sparql", + 'getRestoredCatsIterator', + 'handleRestores', + [ + (object)[ + 'rc_title' => 'Restored cat', + 'rc_cur_id' => 10, + '_processed' => 10, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Restored again', + 'rc_cur_id' => 10, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Already seen', + 'rc_cur_id' => 11, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + ], + [ 11 => true ], + ], + 'new page' => [ + __DIR__ . "/../data/categoriesrdf/new.sparql", + 'getNewCatsIterator', + 'handleAdds', + [ + (object)[ + 'rc_title' => 'New category', + 'rc_cur_id' => 20, + '_processed' => 20, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Новая категория 😃', + 'rc_cur_id' => 21, + '_processed' => 21, + 'pp_propname' => 'hiddencat', + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Processed already', + 'rc_cur_id' => 22, + ], + ], + [ 22 => true ], + ], + 'edit category' => [ + __DIR__ . "/../data/categoriesrdf/edit.sparql", + 'getChangedCatsIterator', + 'handleEdits', + [ + (object)[ + 'rc_title' => 'Changed category', + 'rc_cur_id' => 30, + '_processed' => 30, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Changed again', + 'rc_cur_id' => 30, + 'pp_propname' => null, + 'cat_pages' => 12, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + (object)[ + 'rc_title' => 'Processed already', + 'rc_cur_id' => 31, + 'pp_propname' => null, + 'cat_pages' => 10, + 'cat_subcats' => 2, + 'cat_files' => 1, + ], + ], + [ 31 => true ], + ], + // TODO: not sure how to test categorization changes, it uses the database select... + ]; + } + + /** + * Mock category links iterator. + * @param IDatabase $dbr + * @param array $ids + * @return array + */ + public function getCategoryLinksIterator( $dbr, array $ids ) { + $res = []; + foreach ( $ids as $pageid ) { + $res[] = (object)[ 'cl_from' => $pageid, 'cl_to' => "Parent of $pageid" ]; + } + return $res; + } + + /** + * @dataProvider provideCategoryData + * @param string $testFileName Name of the test, defines filename with expected results. + * @param string $iterator Iterator method name to mock + * @param string $handler Handler method to call + * @param array $result Result to be returned from mock iterator + * @param array $preProcessed List of pre-processed items + */ + public function testSparqlUpdate( $testFileName, $iterator, $handler, $result, + array $preProcessed = [] ) { + $dumpScript = + $this->getMockBuilder( CategoryChangesAsRdf::class ) + ->setMethods( [ $iterator, 'getCategoryLinksIterator' ] ) + ->getMock(); + + $dumpScript->expects( $this->any() ) + ->method( 'getCategoryLinksIterator' ) + ->willReturnCallback( [ $this, 'getCategoryLinksIterator' ] ); + + $dumpScript->expects( $this->once() ) + ->method( $iterator ) + ->willReturn( [ $result ] ); + + $ref = new ReflectionObject( $dumpScript ); + $processedProperty = $ref->getProperty( 'processed' ); + $processedProperty->setAccessible( true ); + $processedProperty->setValue( $dumpScript, $preProcessed ); + + $output = fopen( "php://memory", "w+b" ); + $dbr = wfGetDB( DB_REPLICA ); + /** @var CategoryChangesAsRdf $dumpScript */ + $dumpScript->initialize(); + $dumpScript->getRdf(); + $dumpScript->$handler( $dbr, $output ); + + rewind( $output ); + $sparql = stream_get_contents( $output ); + $this->assertFileContains( $testFileName, $sparql ); + + $processed = $processedProperty->getValue( $dumpScript ); + $expectedProcessed = array_keys( $preProcessed ); + foreach ( $result as $row ) { + if ( isset( $row->_processed ) ) { + $this->assertArrayHasKey( $row->_processed, $processed, + "ID {$row->_processed} was not processed!" ); + $expectedProcessed[] = $row->_processed; + } + } + $this->assertSame( $expectedProcessed, array_keys( $processed ), + 'Processed array has wrong items' ); + } + + public function testUpdateTs() { + $dumpScript = new CategoryChangesAsRdf(); + $dumpScript->initialize(); + $update = $dumpScript->updateTS( 1503620949 ); + $outFile = __DIR__ . '/../data/categoriesrdf/updatets.txt'; + $this->assertFileContains( $outFile, $update ); + } + +} diff --git a/tests/phpunit/maintenance/categoryChangesRdfTest.php b/tests/phpunit/maintenance/categoryChangesRdfTest.php deleted file mode 100644 index 701929a4b3..0000000000 --- a/tests/phpunit/maintenance/categoryChangesRdfTest.php +++ /dev/null @@ -1,263 +0,0 @@ -setMwGlobals( [ - 'wgServer' => 'http://acme.test', - 'wgCanonicalServer' => 'http://acme.test', - 'wgArticlePath' => '/wiki/$1', - ] ); - } - - public function provideCategoryData() { - return [ - 'delete category' => [ - __DIR__ . "/../data/categoriesrdf/delete.sparql", - 'getDeletedCatsIterator', - 'handleDeletes', - [ - (object)[ 'rc_title' => 'Test', 'rc_cur_id' => 1, '_processed' => 1 ], - (object)[ 'rc_title' => 'Test 2', 'rc_cur_id' => 2, '_processed' => 2 ], - ], - ], - 'move category' => [ - __DIR__ . "/../data/categoriesrdf/move.sparql", - 'getMovedCatsIterator', - 'handleMoves', - [ - (object)[ - 'rc_title' => 'Test', - 'rc_cur_id' => 4, - 'page_title' => 'MovedTo', - 'page_namespace' => NS_CATEGORY, - '_processed' => 4, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'MovedTo', - 'rc_cur_id' => 4, - 'page_title' => 'MovedAgain', - 'page_namespace' => NS_CATEGORY, - 'pp_propname' => 'hiddencat', - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Test 2', - 'rc_cur_id' => 5, - 'page_title' => 'AlsoMoved', - 'page_namespace' => NS_CATEGORY, - '_processed' => 5, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Test 3', - 'rc_cur_id' => 6, - 'page_title' => 'MovedOut', - 'page_namespace' => NS_MAIN, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Test 4', - 'rc_cur_id' => 7, - 'page_title' => 'Already Done', - 'page_namespace' => NS_CATEGORY, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - ], - [ 7 => true ], - ], - 'restore deleted category' => [ - __DIR__ . "/../data/categoriesrdf/restore.sparql", - 'getRestoredCatsIterator', - 'handleRestores', - [ - (object)[ - 'rc_title' => 'Restored cat', - 'rc_cur_id' => 10, - '_processed' => 10, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Restored again', - 'rc_cur_id' => 10, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Already seen', - 'rc_cur_id' => 11, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - ], - [ 11 => true ], - ], - 'new page' => [ - __DIR__ . "/../data/categoriesrdf/new.sparql", - 'getNewCatsIterator', - 'handleAdds', - [ - (object)[ - 'rc_title' => 'New category', - 'rc_cur_id' => 20, - '_processed' => 20, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Новая категория 😃', - 'rc_cur_id' => 21, - '_processed' => 21, - 'pp_propname' => 'hiddencat', - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Processed already', - 'rc_cur_id' => 22, - ], - ], - [ 22 => true ], - ], - 'edit category' => [ - __DIR__ . "/../data/categoriesrdf/edit.sparql", - 'getChangedCatsIterator', - 'handleEdits', - [ - (object)[ - 'rc_title' => 'Changed category', - 'rc_cur_id' => 30, - '_processed' => 30, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Changed again', - 'rc_cur_id' => 30, - 'pp_propname' => null, - 'cat_pages' => 12, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - (object)[ - 'rc_title' => 'Processed already', - 'rc_cur_id' => 31, - 'pp_propname' => null, - 'cat_pages' => 10, - 'cat_subcats' => 2, - 'cat_files' => 1, - ], - ], - [ 31 => true ], - ], - // TODO: not sure how to test categorization changes, it uses the database select... - ]; - } - - /** - * Mock category links iterator. - * @param $dbr - * @param array $ids - * @return array - */ - public function getCategoryLinksIterator( $dbr, array $ids ) { - $res = []; - foreach ( $ids as $pageid ) { - $res[] = (object)[ 'cl_from' => $pageid, 'cl_to' => "Parent of $pageid" ]; - } - return $res; - } - - /** - * @dataProvider provideCategoryData - * @param string $testFileName Name of the test, defines filename with expected results. - * @param string $iterator Iterator method name to mock - * @param string $handler Handler method to call - * @param array $result Result to be returned from mock iterator - * @param array $preProcessed List of pre-processed items - */ - public function testSparqlUpdate( $testFileName, $iterator, $handler, $result, - array $preProcessed = [] ) { - $dumpScript = - $this->getMockBuilder( CategoryChangesAsRdf::class ) - ->setMethods( [ $iterator, 'getCategoryLinksIterator' ] ) - ->getMock(); - - $dumpScript->expects( $this->any() ) - ->method( 'getCategoryLinksIterator' ) - ->willReturnCallback( [ $this, 'getCategoryLinksIterator' ] ); - - $dumpScript->expects( $this->once() ) - ->method( $iterator ) - ->willReturn( [ $result ] ); - - $ref = new ReflectionObject( $dumpScript ); - $processedProperty = $ref->getProperty( 'processed' ); - $processedProperty->setAccessible( true ); - $processedProperty->setValue( $dumpScript, $preProcessed ); - - $output = fopen( "php://memory", "w+b" ); - $dbr = wfGetDB( DB_REPLICA ); - /** @var CategoryChangesAsRdf $dumpScript */ - $dumpScript->initialize(); - $dumpScript->getRdf(); - $dumpScript->$handler( $dbr, $output ); - - rewind( $output ); - $sparql = stream_get_contents( $output ); - $this->assertFileContains( $testFileName, $sparql ); - - $processed = $processedProperty->getValue( $dumpScript ); - $expectedProcessed = $preProcessed; - foreach ( $result as $row ) { - if ( isset( $row->_processed ) ) { - $this->assertArrayHasKey( $row->_processed, $processed, - "ID {$row->_processed} was not processed!" ); - $expectedProcessed[] = $row->_processed; - } - } - $this->assertArrayEquals( $expectedProcessed, array_keys( $processed ), - 'Processed array has wrong items' ); - } - - public function testUpdateTs() { - $dumpScript = new CategoryChangesAsRdf(); - $dumpScript->initialize(); - $update = $dumpScript->updateTS( 1503620949 ); - $outFile = __DIR__ . '/../data/categoriesrdf/updatets.txt'; - $this->assertFileContains( $outFile, $update ); - } - -} diff --git a/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php b/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php index 42569d782d..b8d13838e9 100644 --- a/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php +++ b/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php @@ -30,6 +30,9 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { $dbw = wfGetDB( DB_MASTER ); $logs = []; + $comment = \MediaWiki\MediaWikiServices::getInstance()->getCommentStore() + ->createComment( $dbw, '' ); + // Manual patrolling $logs[] = [ 'log_type' => 'patrol', @@ -39,6 +42,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20041223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Autopatrol #1 @@ -50,6 +54,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20051223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Block @@ -61,6 +66,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20061223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Very old/ invalid patrol @@ -72,6 +78,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20061223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Autopatrol #2 @@ -83,6 +90,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20071223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Autopatrol #3 old way @@ -94,6 +102,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20081223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Manual patrol #2 old way @@ -105,6 +114,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20091223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Autopatrol #4 very old way @@ -116,6 +126,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20081223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; // Manual patrol #3 very old way @@ -127,6 +138,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { 'log_timestamp' => $dbw->timestamp( '20091223210426' ), 'log_namespace' => NS_MAIN, 'log_title' => 'DeleteAutoPatrolLogs', + 'log_comment_id' => $comment->id, ]; $dbw->insert( 'logging', $logs ); diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php index 97e0c88fd8..8eadb0ead1 100644 --- a/tests/phpunit/maintenance/fetchTextTest.php +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -4,14 +4,13 @@ namespace MediaWiki\Tests\Maintenance; use ContentHandler; use FetchText; +use MediaWiki\Storage\RevisionRecord; use MediaWikiTestCase; use MWException; use Title; use PHPUnit_Framework_ExpectationFailedException; use WikiPage; -require_once __DIR__ . "/../../../maintenance/fetchText.php"; - /** * Mock for the input/output of FetchText * @@ -106,12 +105,12 @@ class FetchTextTest extends MediaWikiTestCase { private $fetchText; /** - * Adds a revision to a page, while returning the resuting text's id + * Adds a revision to a page and returns the main slot's blob address * * @param WikiPage $page The page to add the revision to * @param string $text The revisions text * @param string $summary The revisions summare - * @return int + * @return string * @throws MWException */ private function addRevision( $page, $text, $summary ) { @@ -122,15 +121,14 @@ class FetchTextTest extends MediaWikiTestCase { if ( $status->isGood() ) { $value = $status->getValue(); - $revision = $value['revision']; - $id = $revision->getTextId(); - if ( $id > 0 ) { - return $id; - } + /** @var RevisionRecord $revision */ + $revision = $value['revision-record']; + $address = $revision->getSlot( 'main' )->getAddress(); + return $address; } - throw new MWException( "Could not determine text id" ); + throw new MWException( "Could not create revision" ); } function addDBDataOnce() { @@ -213,6 +211,11 @@ class FetchTextTest extends MediaWikiTestCase { self::$textId2 . "\n23\nFetchTextTestPage2Text1" ); } + function testExistingInteger() { + $this->assertFilter( (int)preg_replace( '/^tt:/', '', self::$textId2 ), + self::$textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + function testExistingSeveral() { $this->assertFilter( implode( "\n", [ @@ -235,36 +238,52 @@ class FetchTextTest extends MediaWikiTestCase { } function testNonExisting() { - $this->assertFilter( self::$textId5 + 10, ( self::$textId5 + 10 ) . "\n-1\n" ); + \Wikimedia\suppressWarnings(); + $this->assertFilter( 'tt:77889911', 'tt:77889911' . "\n-1\n" ); + \Wikimedia\suppressWarnings( true ); + } + + function testNonExistingInteger() { + \Wikimedia\suppressWarnings(); + $this->assertFilter( '77889911', 'tt:77889911' . "\n-1\n" ); + \Wikimedia\suppressWarnings( true ); + } + + function testBadBlobAddressWithColon() { + $this->assertFilter( 'foo:bar', 'foo:bar' . "\n-1\n" ); } function testNegativeInteger() { - $this->assertFilter( "-42", "-42\n-1\n" ); + $this->assertFilter( "-42", "tt:-42\n-1\n" ); } function testFloatingPointNumberExisting() { - // float -> int -> revision - $this->assertFilter( self::$textId3 + 0.14159, + // float -> int -> address -> revision + $id = intval( preg_replace( '/^tt:/', '', self::$textId3 ) ) + 0.14159; + $this->assertFilter( 'tt:' . intval( $id ), self::$textId3 . "\n23\nFetchTextTestPage2Text2" ); } function testFloatingPointNumberNonExisting() { - $this->assertFilter( self::$textId5 + 3.14159, - ( self::$textId5 + 3 ) . "\n-1\n" ); + \Wikimedia\suppressWarnings(); + $id = intval( preg_replace( '/^tt:/', '', self::$textId5 ) ) + 3.14159; + $this->assertFilter( $id, 'tt:' . intval( $id ) . "\n-1\n" ); + \Wikimedia\suppressWarnings( true ); } function testCharacters() { - $this->assertFilter( "abc", "0\n-1\n" ); + $this->assertFilter( "abc", "abc\n-1\n" ); } function testMix() { - $this->assertFilter( "ab\n" . self::$textId4 . ".5cd\n\nefg\n" . self::$textId2 + $this->assertFilter( "ab\n" . self::$textId4 . ".5cd\n\nefg\nfoo:bar\n" . self::$textId2 . "\n" . self::$textId3, implode( "", [ - "0\n-1\n", - self::$textId4 . "\n23\nFetchTextTestPage2Text3", - "0\n-1\n", - "0\n-1\n", + "ab\n-1\n", + self::$textId4 . ".5cd\n-1\n", + "\n-1\n", + "efg\n-1\n", + "foo:bar\n-1\n", self::$textId2 . "\n23\nFetchTextTestPage2Text1", self::$textId3 . "\n23\nFetchTextTestPage2Text2" ] ) ); diff --git a/tests/phpunit/maintenance/xml.xsd b/tests/phpunit/maintenance/xml.xsd new file mode 100644 index 0000000000..aea7d0db0a --- /dev/null +++ b/tests/phpunit/maintenance/xml.xsd @@ -0,0 +1,287 @@ + + + + + + +
    +

    About the XML namespace

    + +
    +

    + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

    +

    + See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

    +

    + Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

    +

    + See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

    +
    +
    +
    +
    + + + + +
    + +

    lang (as an attribute name)

    +

    + denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

    + +
    +
    +

    Notes

    +

    + Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

    +

    + See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

    +

    + The union allows for the 'un-declaration' of xml:lang with + the empty string. +

    +
    +
    +
    + + + + + + + + + +
    + + + + +
    + +

    space (as an attribute name)

    +

    + denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

    + +
    +
    +
    + + + + + + +
    + + + +
    + +

    base (as an attribute name)

    +

    + denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

    + +

    + See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

    +
    +
    +
    +
    + + + + +
    + +

    id (as an attribute name)

    +

    + denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

    + +

    + See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

    +
    +
    +
    +
    + + + + + + + + + + +
    + +

    Father (in any context at all)

    + +
    +

    + denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

    +
    +

    + In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

    +
    +
    +
    +
    +
    + + + +
    +

    About this schema document

    + +
    +

    + This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

    +

    + To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

    +
    +          <schema . . .>
    +           . . .
    +           <import namespace="http://www.w3.org/XML/1998/namespace"
    +                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
    +     
    +

    + or +

    +
    +           <import namespace="http://www.w3.org/XML/1998/namespace"
    +                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
    +     
    +

    + Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

    +
    +          <type . . .>
    +           . . .
    +           <attributeGroup ref="xml:specialAttrs"/>
    +     
    +

    + will define a type which will schema-validate an instance element + with any of those attributes. +

    +
    +
    +
    +
    + + + +
    +

    Versioning policy for this schema document

    +
    +

    + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

    +

    + At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

    +

    + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

    +

    + Previous dated (and unchanging) versions of this schema + document are at: +

    + +
    +
    +
    +
    + +
    + diff --git a/tests/phpunit/mocks/content/DummyContentForTesting.php b/tests/phpunit/mocks/content/DummyContentForTesting.php index 6bc7c4487c..e3cac83c82 100644 --- a/tests/phpunit/mocks/content/DummyContentForTesting.php +++ b/tests/phpunit/mocks/content/DummyContentForTesting.php @@ -104,7 +104,7 @@ class DummyContentForTesting extends AbstractContent { public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) { - return new ParserOutput( $this->getNativeData() ); + return new ParserOutput( $this->data ); } /** @@ -118,6 +118,6 @@ class DummyContentForTesting extends AbstractContent { */ protected function fillParserOutput( Title $title, $revId, ParserOptions $options, $generateHtml, ParserOutput &$output ) { - $output = new ParserOutput( $this->getNativeData() ); + $output = new ParserOutput( $this->data ); } } diff --git a/tests/phpunit/mocks/content/DummyNonTextContent.php b/tests/phpunit/mocks/content/DummyNonTextContent.php index e65f522640..bdfa8d072b 100644 --- a/tests/phpunit/mocks/content/DummyNonTextContent.php +++ b/tests/phpunit/mocks/content/DummyNonTextContent.php @@ -102,7 +102,7 @@ class DummyNonTextContent extends AbstractContent { public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) { - return new ParserOutput( $this->getNativeData() ); + return new ParserOutput( $this->serialize() ); } /** @@ -116,6 +116,6 @@ class DummyNonTextContent extends AbstractContent { */ protected function fillParserOutput( Title $title, $revId, ParserOptions $options, $generateHtml, ParserOutput &$output ) { - $output = new ParserOutput( $this->getNativeData() ); + $output = new ParserOutput( $this->serialize() ); } } diff --git a/tests/phpunit/mocks/media/MockDjVuHandler.php b/tests/phpunit/mocks/media/MockDjVuHandler.php index 0e0b9435cd..29cc6b3c07 100644 --- a/tests/phpunit/mocks/media/MockDjVuHandler.php +++ b/tests/phpunit/mocks/media/MockDjVuHandler.php @@ -22,7 +22,7 @@ */ class MockDjVuHandler extends DjVuHandler { - function isEnabled() { + public function isEnabled() { return true; } diff --git a/tests/phpunit/skins/SideBarTest.php b/tests/phpunit/skins/SideBarTest.php index ec85bb0326..22ca60f7bc 100644 --- a/tests/phpunit/skins/SideBarTest.php +++ b/tests/phpunit/skins/SideBarTest.php @@ -170,6 +170,7 @@ class SideBarTest extends MediaWikiLangTestCase { /** * Simple test to verify our helper assertAttribs() is functional + * @coversNothing */ public function testTestAttributesAssertionHelper() { $this->setMwGlobals( [ diff --git a/tests/phpunit/structure/ApiStructureTest.php b/tests/phpunit/structure/ApiStructureTest.php index 95d3b60b00..2453353b85 100644 --- a/tests/phpunit/structure/ApiStructureTest.php +++ b/tests/phpunit/structure/ApiStructureTest.php @@ -10,6 +10,7 @@ use Wikimedia\TestingAccessWrapper; * - do not have inconsistencies in the parameter definitions * * @group API + * @coversNothing */ class ApiStructureTest extends MediaWikiTestCase { @@ -499,10 +500,8 @@ class ApiStructureTest extends MediaWikiTestCase { if ( $value instanceof $type ) { return; } - } else { - if ( gettype( $value ) === $type ) { - return; - } + } elseif ( gettype( $value ) === $type ) { + return; } } else { // Array whose values have specified types, recurse diff --git a/tests/phpunit/structure/AutoLoaderStructureTest.php b/tests/phpunit/structure/AutoLoaderStructureTest.php index 75e21ae085..2ae6a78b74 100644 --- a/tests/phpunit/structure/AutoLoaderStructureTest.php +++ b/tests/phpunit/structure/AutoLoaderStructureTest.php @@ -112,14 +112,12 @@ class AutoLoaderStructureTest extends MediaWikiTestCase { // 'class Foo {}' $class = $fileNamespace . $match['class']; $classesInFile[$class] = true; + } elseif ( !empty( $match['original'] ) ) { + // 'class_alias( "Foo", "Bar" );' + $aliasesInFile[$match['alias']] = $match['original']; } else { - if ( !empty( $match['original'] ) ) { - // 'class_alias( "Foo", "Bar" );' - $aliasesInFile[$match['alias']] = $match['original']; - } else { - // 'class_alias( Foo::class, "Bar" );' - $aliasesInFile[$match['aliasString']] = $fileNamespace . $match['originalStatic']; - } + // 'class_alias( Foo::class, "Bar" );' + $aliasesInFile[$match['aliasString']] = $fileNamespace . $match['originalStatic']; } } diff --git a/tests/phpunit/structure/DatabaseIntegrationTest.php b/tests/phpunit/structure/DatabaseIntegrationTest.php index b0c1c8f1f5..9c0a73de8d 100644 --- a/tests/phpunit/structure/DatabaseIntegrationTest.php +++ b/tests/phpunit/structure/DatabaseIntegrationTest.php @@ -5,6 +5,7 @@ use Wikimedia\Rdbms\Database; /** * @group Database + * @coversNothing */ class DatabaseIntegrationTest extends MediaWikiTestCase { /** diff --git a/tests/phpunit/structure/PasswordPolicyStructureTest.php b/tests/phpunit/structure/PasswordPolicyStructureTest.php new file mode 100644 index 0000000000..60ce575e4c --- /dev/null +++ b/tests/phpunit/structure/PasswordPolicyStructureTest.php @@ -0,0 +1,49 @@ + $callback ) { + yield [ $name ]; + } + } + + public function provideFlags() { + global $wgPasswordPolicy; + + // This won't actually find all flags, just the ones in use. Can't really be helped, + // other than adding the core flags here. + $flags = [ 'forceChange', 'suggestChangeOnLogin' ]; + foreach ( $wgPasswordPolicy['policies'] as $group => $checks ) { + foreach ( $checks as $check => $settings ) { + if ( is_array( $settings ) ) { + $flags = array_unique( + array_merge( $flags, array_diff( array_keys( $settings ), [ 'value' ] ) ) + ); + } + } + } + + foreach ( $flags as $flag ) { + yield [ $flag ]; + } + } + + /** @dataProvider provideChecks */ + public function testCheckMessage( $check ) { + $msg = wfMessage( 'passwordpolicies-policy-' . strtolower( $check ) ); + $this->assertTrue( $msg->exists() ); + } + + /** @dataProvider provideFlags */ + public function testFlagMessage( $flag ) { + $msg = wfMessage( 'passwordpolicies-policyflag-' . strtolower( $flag ) ); + $this->assertTrue( $msg->exists() ); + } + +} diff --git a/tests/phpunit/structure/ResourcesTest.php b/tests/phpunit/structure/ResourcesTest.php index 8a08181c1e..f41ab3a11f 100644 --- a/tests/phpunit/structure/ResourcesTest.php +++ b/tests/phpunit/structure/ResourcesTest.php @@ -1,8 +1,11 @@ assertArrayEquals( + $this->assertSame( $expected, $files, 'Url(...) expression in comment should be omitted.' @@ -171,8 +174,8 @@ class ResourcesTest extends MediaWikiTestCase { $org_wgEnableJavaScriptTest = $wgEnableJavaScriptTest; $wgEnableJavaScriptTest = true; - // Initialize ResourceLoader - $rl = new ResourceLoader(); + // Get main ResourceLoader + $rl = MediaWikiServices::getInstance()->getResourceLoader(); $modules = []; @@ -243,9 +246,6 @@ class ResourcesTest extends MediaWikiTestCase { /** * Get all resource files from modules that are an instance of * ResourceLoaderFileModule (or one of its subclasses). - * - * Since the raw data is stored in protected properties, we have to - * overrride this through ReflectionObject methods. */ public static function provideResourceFiles() { $data = self::getAllModules(); @@ -273,14 +273,12 @@ class ResourcesTest extends MediaWikiTestCase { continue; } - $reflectedModule = new ReflectionObject( $module ); + $moduleProxy = TestingAccessWrapper::newFromObject( $module ); $files = []; foreach ( $filePathProps['lists'] as $propName ) { - $property = $reflectedModule->getProperty( $propName ); - $property->setAccessible( true ); - $list = $property->getValue( $module ); + $list = $moduleProxy->$propName; foreach ( $list as $key => $value ) { // 'scripts' are numeral arrays. // 'styles' can be numeral or associative. @@ -295,9 +293,7 @@ class ResourcesTest extends MediaWikiTestCase { } foreach ( $filePathProps['nested-lists'] as $propName ) { - $property = $reflectedModule->getProperty( $propName ); - $property->setAccessible( true ); - $lists = $property->getValue( $module ); + $lists = $moduleProxy->$propName; foreach ( $lists as $list ) { foreach ( $list as $key => $value ) { // We need the same filter as for 'lists', @@ -311,29 +307,23 @@ class ResourcesTest extends MediaWikiTestCase { } } - // Get method for resolving the paths to full paths - $method = $reflectedModule->getMethod( 'getLocalPath' ); - $method->setAccessible( true ); - // Populate cases foreach ( $files as $file ) { $cases[] = [ - $method->invoke( $module, $file ), + $moduleProxy->getLocalPath( $file ), $moduleName, ( $file instanceof ResourceLoaderFilePath ? $file->getPath() : $file ), ]; } // To populate missingLocalFileRefs. Not sure how sane this is inside this test... - $module->readStyleFiles( + $moduleProxy->readStyleFiles( $module->getStyleFiles( $data['context'] ), $module->getFlip( $data['context'] ), $data['context'] ); - $property = $reflectedModule->getProperty( 'missingLocalFileRefs' ); - $property->setAccessible( true ); - $missingLocalFileRefs = $property->getValue( $module ); + $missingLocalFileRefs = $moduleProxy->missingLocalFileRefs; foreach ( $missingLocalFileRefs as $file ) { $cases[] = [ diff --git a/tests/phpunit/structure/SpecialPageFatalTest.php b/tests/phpunit/structure/SpecialPageFatalTest.php index a6bc5a7f0e..97797cacf6 100644 --- a/tests/phpunit/structure/SpecialPageFatalTest.php +++ b/tests/phpunit/structure/SpecialPageFatalTest.php @@ -11,6 +11,7 @@ use MediaWiki\MediaWikiServices; * * @since 1.32 * @author Addshore + * @coversNothing */ class SpecialPageFatalTest extends MediaWikiTestCase { public function provideSpecialPages() { @@ -31,8 +32,14 @@ class SpecialPageFatalTest extends MediaWikiTestCase { try { $executor->executeSpecialPage( $page, '', null, null, $user ); + } catch ( \PHPUnit\Framework\Error\Error $error ) { + // Let phpunit settings working: + // - convertErrorsToExceptions="true" + // - convertNoticesToExceptions="true" + // - convertWarningsToExceptions="true" + throw $error; } catch ( Exception $e ) { - // Exceptions are allowed + // Other exceptions are allowed } // If the page fataled phpunit will have already died diff --git a/tests/phpunit/suites/LessTestSuite.php b/tests/phpunit/suites/LessTestSuite.php index 26a784adca..b5bd882d50 100644 --- a/tests/phpunit/suites/LessTestSuite.php +++ b/tests/phpunit/suites/LessTestSuite.php @@ -1,5 +1,7 @@ */ @@ -7,7 +9,7 @@ class LessTestSuite extends PHPUnit_Framework_TestSuite { public function __construct() { parent::__construct(); - $resourceLoader = new ResourceLoader(); + $resourceLoader = MediaWikiServices::getInstance()->getResourceLoader(); foreach ( $resourceLoader->getModuleNames() as $name ) { $module = $resourceLoader->getModule( $name ); diff --git a/tests/phpunit/tests/MediaWikiTestCaseTest.php b/tests/phpunit/tests/MediaWikiTestCaseTest.php index 599b733e05..6b5a487634 100644 --- a/tests/phpunit/tests/MediaWikiTestCaseTest.php +++ b/tests/phpunit/tests/MediaWikiTestCaseTest.php @@ -7,6 +7,7 @@ use Wikimedia\Rdbms\LoadBalancer; /** * @covers MediaWikiTestCase * @group MediaWikiTestCaseTest + * @group Database * * @author Addshore */ @@ -173,4 +174,38 @@ class MediaWikiTestCaseTest extends MediaWikiTestCase { $this->assertSame( $logger1, $logger2 ); } + + /** + * @covers MediaWikiTestCase::setupDatabaseWithTestPrefix + * @covers MediaWikiTestCase::copyTestData + */ + public function testCopyTestData() { + $this->markTestSkippedIfDbType( 'sqlite' ); + + $this->tablesUsed[] = 'objectcache'; + $this->db->insert( + 'objectcache', + [ 'keyname' => __METHOD__, 'value' => 'TEST', 'exptime' => $this->db->timestamp( 11 ) ], + __METHOD__ + ); + + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lb = $lbFactory->newMainLB(); + $db = $lb->getConnection( DB_REPLICA, DBO_TRX ); + + // sanity + $this->assertNotSame( $this->db, $db ); + + // Make sure the DB connection has the fake table clones and the fake table prefix + MediaWikiTestCase::setupDatabaseWithTestPrefix( $db, $this->dbPrefix(), false ); + + $this->assertSame( $this->db->tablePrefix(), $db->tablePrefix(), 'tablePrefix' ); + + // Make sure the DB connection has all the test data + $this->copyTestData( $this->db, $db ); + + $value = $db->selectField( 'objectcache', 'value', [ 'keyname' => __METHOD__ ], __METHOD__ ); + $this->assertSame( 'TEST', $value, 'Copied Data' ); + } + } diff --git a/tests/qunit/.eslintrc.json b/tests/qunit/.eslintrc.json index 03b02ba9ca..bce5b1600b 100644 --- a/tests/qunit/.eslintrc.json +++ b/tests/qunit/.eslintrc.json @@ -12,6 +12,6 @@ "valid-jsdoc": "off", "qunit/require-expect": "off", "qunit/resolve-async": "off", - "jquery/no-parse-html-literal": "off" + "no-jquery/no-parse-html-literal": "off" } } diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index d6ede4f53b..4969a8b45f 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -41,7 +41,6 @@ return [ 'tests/qunit/suites/resources/jquery/jquery.color.test.js', 'tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js', 'tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js', - 'tests/qunit/suites/resources/jquery/jquery.hidpi.test.js', 'tests/qunit/suites/resources/jquery/jquery.highlightText.test.js', 'tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js', 'tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js', @@ -101,7 +100,6 @@ return [ 'jquery.color', 'jquery.colorUtil', 'jquery.getAttrs', - 'jquery.hidpi', 'jquery.highlightText', 'jquery.lengthLimit', 'jquery.makeCollapsible', diff --git a/tests/qunit/data/mediawiki.loader.getScript.example.js b/tests/qunit/data/mediawiki.loader.getScript.example.js new file mode 100644 index 0000000000..e5e4759d1c --- /dev/null +++ b/tests/qunit/data/mediawiki.loader.getScript.example.js @@ -0,0 +1 @@ +mw.getScriptExampleScriptLoaded = true; diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js index df3d61b722..3e52d8b8de 100644 --- a/tests/qunit/data/testrunner.js +++ b/tests/qunit/data/testrunner.js @@ -258,7 +258,7 @@ // Check for incomplete animations/requests/etc and throw if there are any. if ( $.timers && $.timers.length !== 0 ) { timers = $.timers.length; - // eslint-disable-next-line jquery/no-each-util + // eslint-disable-next-line no-jquery/no-each-util $.each( $.timers, function ( i, timer ) { var node = timer.elem; mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' + @@ -307,7 +307,7 @@ var altPromises = []; // When we have ES6 support we'll be able to use Array.from here - // eslint-disable-next-line jquery/no-each-util + // eslint-disable-next-line no-jquery/no-each-util $.each( arguments, function ( i, arg ) { var alt = $.Deferred(); altPromises.push( alt ); diff --git a/tests/qunit/suites/resources/jquery/jquery.color.test.js b/tests/qunit/suites/resources/jquery/jquery.color.test.js index 2e35420dfd..fdde3bada8 100644 --- a/tests/qunit/suites/resources/jquery/jquery.color.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.color.test.js @@ -5,7 +5,7 @@ var done = assert.async(), $canvas = $( '
    ' ).css( 'background-color', '#fff' ).appendTo( '#qunit-fixture' ); - // eslint-disable-next-line jquery/no-animate + // eslint-disable-next-line no-jquery/no-animate $canvas.animate( { 'background-color': '#000' }, 3 ).promise() .done( function () { var endColors = $.colorUtil.getRGB( $canvas.css( 'background-color' ) ); diff --git a/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js b/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js deleted file mode 100644 index cb09180b21..0000000000 --- a/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js +++ /dev/null @@ -1,38 +0,0 @@ -( function () { - QUnit.module( 'jquery.hidpi', QUnit.newMwEnvironment() ); - - QUnit.test( 'devicePixelRatio', function ( assert ) { - var devicePixelRatio = $.devicePixelRatio(); - assert.strictEqual( typeof devicePixelRatio, 'number', '$.devicePixelRatio() returns a number' ); - } ); - - QUnit.test( 'bracketedDevicePixelRatio', function ( assert ) { - var ratio = $.bracketedDevicePixelRatio(); - assert.strictEqual( typeof ratio, 'number', '$.bracketedDevicePixelRatio() returns a number' ); - } ); - - QUnit.test( 'bracketDevicePixelRatio', function ( assert ) { - assert.strictEqual( $.bracketDevicePixelRatio( 0.75 ), 1, '0.75 gives 1' ); - assert.strictEqual( $.bracketDevicePixelRatio( 1 ), 1, '1 gives 1' ); - assert.strictEqual( $.bracketDevicePixelRatio( 1.25 ), 1.5, '1.25 gives 1.5' ); - assert.strictEqual( $.bracketDevicePixelRatio( 1.5 ), 1.5, '1.5 gives 1.5' ); - assert.strictEqual( $.bracketDevicePixelRatio( 1.75 ), 2, '1.75 gives 2' ); - assert.strictEqual( $.bracketDevicePixelRatio( 2 ), 2, '2 gives 2' ); - assert.strictEqual( $.bracketDevicePixelRatio( 2.5 ), 2, '2.5 gives 2' ); - assert.strictEqual( $.bracketDevicePixelRatio( 3 ), 2, '3 gives 2' ); - } ); - - QUnit.test( 'matchSrcSet', function ( assert ) { - var srcset = 'onefive.png 1.5x, two.png 2x'; - - // Nice exact matches - assert.strictEqual( $.matchSrcSet( 1, srcset ), null, '1.0 gives no match' ); - assert.strictEqual( $.matchSrcSet( 1.5, srcset ), 'onefive.png', '1.5 gives match' ); - assert.strictEqual( $.matchSrcSet( 2, srcset ), 'two.png', '2 gives match' ); - - // Non-exact matches; should return the next-biggest specified - assert.strictEqual( $.matchSrcSet( 1.25, srcset ), null, '1.25 gives no match' ); - assert.strictEqual( $.matchSrcSet( 1.75, srcset ), 'onefive.png', '1.75 gives match to 1.5' ); - assert.strictEqual( $.matchSrcSet( 2.25, srcset ), 'two.png', '2.25 gives match to 2' ); - } ); -}() ); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js index 549deb0e1b..5691a1b065 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js @@ -1,5 +1,8 @@ ( function () { QUnit.module( 'mediawiki.api.options', QUnit.newMwEnvironment( { + config: { + wgUserName: 'Foo' + }, setup: function () { this.server = this.sandbox.useFakeServer(); this.server.respondImmediately = true; @@ -138,4 +141,21 @@ } ) ); } ); + + QUnit.test( 'saveOptions (anonymous)', function ( assert ) { + var promise, test = this; + + mw.config.set( 'wgUserName', null ); + promise = new mw.Api().saveOptions( { foo: 'bar' } ); + + assert.rejects( promise, /notloggedin/, 'Can not save options while not logged in' ); + + return promise + .catch( function () { + return $.Deferred().resolve(); + } ) + .then( function () { + assert.strictEqual( test.server.requests.length, 0, 'No requests made' ); + } ); + } ); }() ); diff --git a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js index 458df92112..aafcd5b217 100644 --- a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js +++ b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js @@ -3,7 +3,7 @@ // TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ] - QUnit.test( '"all" namespace disable checkboxes', function ( assert ) { + QUnit.test( '"all" namespace hides checkboxes', function ( assert ) { var selectHtml, $env, $options, rc = require( 'mediawiki.special.recentchanges' ); @@ -17,10 +17,14 @@ + '' + '' + '' + + '' + '' + '' + + '' + + '' + '' + '' + + '' + '' + ''; @@ -28,16 +32,16 @@ // TODO abstract the double strictEquals - // At first checkboxes are enabled - assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false ); - assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false ); + // At first checkboxes are hidden + assert.strictEqual( $( '#nsinvert' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), true ); + assert.strictEqual( $( '#nsassociated' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), true ); // Initiate the recentchanges module rc.init(); // By default - assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true ); - assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true ); + assert.strictEqual( $( '#nsinvert' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), true ); + assert.strictEqual( $( '#nsassociated' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), true ); // select second option... $options = $( '#namespace' ).find( 'option' ); @@ -45,18 +49,18 @@ $options.eq( 1 ).prop( 'selected', true ); $( '#namespace' ).trigger( 'change' ); - // ... and checkboxes should be enabled again - assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false ); - assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false ); + // ... and checkboxes should be visible again + assert.strictEqual( $( '#nsinvert' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), false ); + assert.strictEqual( $( '#nsassociated' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), false ); // select first option ( 'all' namespace)... $options.eq( 1 ).removeProp( 'selected' ); $options.eq( 0 ).prop( 'selected', true ); $( '#namespace' ).trigger( 'change' ); - // ... and checkboxes should now be disabled - assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true ); - assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true ); + // ... and checkboxes should now be hidden + assert.strictEqual( $( '#nsinvert' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), true ); + assert.strictEqual( $( '#nsassociated' ).closest( '.mw-input-with-label' ).hasClass( 'mw-input-hidden' ), true ); // DOM cleanup $env.remove(); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js index 84e1d4eb5a..fca1f7d016 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js @@ -324,19 +324,14 @@ } ); QUnit.test( 'wantSignaturesNamespace', function ( assert ) { - var namespaces = mw.config.values.wgExtraSignatureNamespaces; - - mw.config.values.wgExtraSignatureNamespaces = []; + mw.config.set( 'wgExtraSignatureNamespaces', [] ); assert.strictEqual( mw.Title.wantSignaturesNamespace( 0 ), false, 'Main namespace has no signatures' ); assert.strictEqual( mw.Title.wantSignaturesNamespace( 1 ), true, 'Talk namespace has signatures' ); assert.strictEqual( mw.Title.wantSignaturesNamespace( 2 ), false, 'NS2 has no signatures' ); assert.strictEqual( mw.Title.wantSignaturesNamespace( 3 ), true, 'NS3 has signatures' ); - mw.config.values.wgExtraSignatureNamespaces = [ 0 ]; + mw.config.set( 'wgExtraSignatureNamespaces', [ 0 ] ); assert.strictEqual( mw.Title.wantSignaturesNamespace( 0 ), true, 'Main namespace has signatures when explicitly defined' ); - - // Restore - mw.config.values.wgExtraSignatureNamespaces = namespaces; } ); QUnit.test( 'Throw error on invalid title', function ( assert ) { diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js index 56801dec20..a237c7edbd 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js @@ -74,7 +74,7 @@ } ); } - // eslint-disable-next-line jquery/no-each-util + // eslint-disable-next-line no-jquery/no-each-util $.each( pluralTestcases, function ( langCode, tests ) { if ( langCode === mw.config.get( 'wgUserLanguage' ) ) { pluralTest( langCode, tests ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js index 4606cbd3df..b3f04b705c 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js @@ -2,23 +2,31 @@ var NOW = 9012, // miliseconds DEFAULT_DURATION = 5678, // seconds + jqcookie, + defaults = { + prefix: 'mywiki', + domain: 'example.org', + path: '/path', + expires: DEFAULT_DURATION, + secure: false + }, + setDefaults = require( 'mediawiki.cookie' ).setDefaults, expiryDate = new Date(); expiryDate.setTime( NOW + ( DEFAULT_DURATION * 1000 ) ); - QUnit.module( 'mediawiki.cookie', QUnit.newMwEnvironment( { - setup: function () { - this.stub( $, 'cookie' ).returns( null ); - - this.sandbox.useFakeTimers( NOW ); + QUnit.module( 'mediawiki.cookie', { + beforeEach: function () { + jqcookie = sinon.stub( $, 'cookie' ).returns( null ); + this.clock = sinon.useFakeTimers( NOW ); + this.savedDefaults = setDefaults( defaults ); }, - config: { - wgCookiePrefix: 'mywiki', - wgCookieDomain: 'example.org', - wgCookiePath: '/path', - wgCookieExpiration: DEFAULT_DURATION + afterEach: function () { + jqcookie.restore(); + this.clock.restore(); + setDefaults( this.savedDefaults ); } - } ) ); + } ); QUnit.test( 'set( key, value )', function ( assert ) { var call; @@ -26,7 +34,7 @@ // Simple case mw.cookie.set( 'foo', 'bar' ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 0 ], 'mywikifoo' ); assert.strictEqual( call[ 1 ], 'bar' ); assert.deepEqual( call[ 2 ], { @@ -37,19 +45,19 @@ } ); mw.cookie.set( 'foo', null ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 1 ], null, 'null removes cookie' ); mw.cookie.set( 'foo', undefined ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 1 ], 'undefined', 'undefined is value' ); mw.cookie.set( 'foo', false ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 1 ], 'false', 'false is a value' ); mw.cookie.set( 'foo', 0 ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 1 ], '0', '0 is value' ); } ); @@ -60,34 +68,34 @@ date.setTime( 1234 ); mw.cookie.set( 'foo', 'bar' ); - options = $.cookie.lastCall.args[ 2 ]; + options = jqcookie.lastCall.args[ 2 ]; assert.deepEqual( options.expires, expiryDate, 'default expiration' ); mw.cookie.set( 'foo', 'bar', date ); - options = $.cookie.lastCall.args[ 2 ]; + options = jqcookie.lastCall.args[ 2 ]; assert.strictEqual( options.expires, date, 'custom expiration as Date' ); date = new Date(); date.setDate( date.getDate() + 1 ); mw.cookie.set( 'foo', 'bar', 86400 ); - options = $.cookie.lastCall.args[ 2 ]; + options = jqcookie.lastCall.args[ 2 ]; assert.deepEqual( options.expires, date, 'custom expiration as lifetime in seconds' ); mw.cookie.set( 'foo', 'bar', null ); - options = $.cookie.lastCall.args[ 2 ]; + options = jqcookie.lastCall.args[ 2 ]; assert.strictEqual( options.expires, undefined, 'null forces session cookie' ); - // Per DefaultSettings.php, when wgCookieExpiration is 0, the default should - // be session cookies - mw.config.set( 'wgCookieExpiration', 0 ); + // Per DefaultSettings.php, if wgCookieExpiration is 0, + // then the default should be session cookies + setDefaults( $.extend( {}, defaults, { expires: 0 } ) ); mw.cookie.set( 'foo', 'bar' ); - options = $.cookie.lastCall.args[ 2 ]; + options = jqcookie.lastCall.args[ 2 ]; assert.strictEqual( options.expires, undefined, 'wgCookieExpiration=0 results in session cookies by default' ); mw.cookie.set( 'foo', 'bar', date ); - options = $.cookie.lastCall.args[ 2 ]; + options = jqcookie.lastCall.args[ 2 ]; assert.strictEqual( options.expires, date, 'custom expiration (with wgCookieExpiration=0)' ); } ); @@ -101,7 +109,7 @@ secure: true } ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 0 ], 'myPrefixfoo' ); assert.deepEqual( call[ 2 ], { expires: expiryDate, @@ -121,7 +129,7 @@ secure: true } ); - call = $.cookie.lastCall.args; + call = jqcookie.lastCall.args; assert.strictEqual( call[ 0 ], 'myPrefixfoo' ); assert.deepEqual( call[ 2 ], { expires: date, @@ -136,19 +144,19 @@ mw.cookie.get( 'foo' ); - key = $.cookie.lastCall.args[ 0 ]; + key = jqcookie.lastCall.args[ 0 ]; assert.strictEqual( key, 'mywikifoo', 'Default prefix' ); mw.cookie.get( 'foo', undefined ); - key = $.cookie.lastCall.args[ 0 ]; + key = jqcookie.lastCall.args[ 0 ]; assert.strictEqual( key, 'mywikifoo', 'Use default prefix for undefined' ); mw.cookie.get( 'foo', null ); - key = $.cookie.lastCall.args[ 0 ]; + key = jqcookie.lastCall.args[ 0 ]; assert.strictEqual( key, 'mywikifoo', 'Use default prefix for null' ); mw.cookie.get( 'foo', '' ); - key = $.cookie.lastCall.args[ 0 ]; + key = jqcookie.lastCall.args[ 0 ]; assert.strictEqual( key, 'foo', 'Don\'t use default prefix for empty string' ); value = mw.cookie.get( 'foo' ); @@ -161,7 +169,7 @@ QUnit.test( 'get( key ) - with value', function ( assert ) { var value; - $.cookie.returns( 'bar' ); + jqcookie.returns( 'bar' ); value = mw.cookie.get( 'foo' ); assert.strictEqual( value, 'bar', 'Return value of cookie' ); @@ -172,7 +180,7 @@ mw.cookie.get( 'foo', 'bar' ); - key = $.cookie.lastCall.args[ 0 ]; + key = jqcookie.lastCall.args[ 0 ]; assert.strictEqual( key, 'barfoo' ); } ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js index 5a4d614b11..3d008f6c28 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js @@ -598,7 +598,7 @@ ] }; - // eslint-disable-next-line jquery/no-each-util + // eslint-disable-next-line no-jquery/no-each-util $.each( grammarTests, function ( langCode, test ) { if ( langCode === mw.config.get( 'wgUserLanguage' ) ) { grammarTest( langCode, test ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js index 11182799b5..e17c78d7af 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js @@ -15,6 +15,7 @@ }; }, teardown: function () { + mw.loader.maxQueryLength = 2000; // Teardown for StringSet shim test if ( this.nativeSet ) { window.Set = this.nativeSet; @@ -24,6 +25,7 @@ // exposed for cross-file mocks. delete mw.loader.testCallback; delete mw.loader.testFail; + delete mw.getScriptExampleScriptLoaded; } } ) ); @@ -558,6 +560,48 @@ } ); } ); + QUnit.test( '.implement( package files )', function ( assert ) { + var done = assert.async(), + initJsRan = false; + mw.loader.implement( + 'test.implement.packageFiles', + { + main: 'resources/src/foo/init.js', + files: { + 'resources/src/foo/data/hello.json': { hello: 'world' }, + 'resources/src/foo/foo.js': function ( require, module ) { + window.mwTestFooJsCounter = window.mwTestFooJsCounter || 41; + window.mwTestFooJsCounter++; + module.exports = { answer: window.mwTestFooJsCounter }; + }, + 'resources/src/bar/bar.js': function ( require, module ) { + var core = require( './core.js' ); + module.exports = { data: core.sayHello( 'Alice' ) }; + }, + 'resources/src/bar/core.js': function ( require, module ) { + module.exports = { sayHello: function ( name ) { + return 'Hello ' + name; + } }; + }, + 'resources/src/foo/init.js': function ( require ) { + initJsRan = true; + assert.deepEqual( require( './data/hello.json' ), { hello: 'world' }, 'require() with .json file' ); + assert.deepEqual( require( './foo.js' ), { answer: 42 }, 'require() with .js file in same directory' ); + assert.deepEqual( require( '../bar/bar.js' ), { data: 'Hello Alice' }, 'require() with ../ of a file that uses same-directory require()' ); + assert.deepEqual( require( './foo.js' ), { answer: 42 }, 'require()ing the same script twice only runs it once' ); + } + } + }, + {}, + {}, + {} + ); + mw.loader.using( 'test.implement.packageFiles' ).done( function () { + assert.ok( initJsRan, 'main JS file is executed' ); + done(); + } ); + } ); + QUnit.test( '.addSource()', function ( assert ) { mw.loader.addSource( { testsource1: 'https://1.test/src' } ); @@ -581,7 +625,7 @@ [ 'testUrlIncDump', 'dump', [], null, 'testloader' ] ] ); - mw.config.set( 'wgResourceLoaderMaxQueryLength', 10 ); + mw.loader.maxQueryLength = 10; return mw.loader.using( [ 'testUrlIncDump', 'testUrlInc' ] ).then( function ( require ) { assert.propEqual( @@ -1055,4 +1099,24 @@ } ); } ); + QUnit.test( '.getScript() - success', function ( assert ) { + var scriptUrl = QUnit.fixurl( + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mediawiki.loader.getScript.example.js' + ); + + return mw.loader.getScript( scriptUrl ).then( + function () { + assert.strictEqual( mw.getScriptExampleScriptLoaded, true, 'Data attached to a global object is available' ); + } + ); + } ); + + QUnit.test( '.getScript() - failure', function ( assert ) { + assert.rejects( + mw.loader.getScript( 'https://example.test/not-found' ), + /Failed to load script/, + 'Descriptive error message' + ); + } ); + }() ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js index 1e3521171b..24876728fd 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js @@ -2,6 +2,7 @@ QUnit.module( 'mediawiki.user', QUnit.newMwEnvironment( { setup: function () { this.server = this.sandbox.useFakeServer(); + this.server.respondImmediately = true; // Cannot stub by simple assignment because read-only. // Instead, stub in tests by using 'delete', and re-create // in teardown using the original descriptor (including its @@ -44,28 +45,45 @@ assert.strictEqual( mw.user.id(), 'John', 'user.id()' ); } ); - QUnit.test( 'getUserInfo', function ( assert ) { + QUnit.test( 'getGroups (callback)', function ( assert ) { + var done = assert.async(); mw.config.set( 'wgUserGroups', [ '*', 'user' ] ); mw.user.getGroups( function ( groups ) { assert.deepEqual( groups, [ '*', 'user' ], 'Result' ); + done(); } ); + } ); + + QUnit.test( 'getGroups (Promise)', function ( assert ) { + mw.config.set( 'wgUserGroups', [ '*', 'user' ] ); + + return mw.user.getGroups().then( function ( groups ) { + assert.deepEqual( groups, [ '*', 'user' ], 'Result' ); + } ); + } ); + + QUnit.test( 'getRights (callback)', function ( assert ) { + var done = assert.async(); + + this.server.respond( [ 200, { 'Content-Type': 'application/json' }, + '{ "query": { "userinfo": { "groups": [ "unused" ], "rights": [ "read", "edit", "createtalk" ] } } }' + ] ); mw.user.getRights( function ( rights ) { assert.deepEqual( rights, [ 'read', 'edit', 'createtalk' ], 'Result (callback)' ); + done(); } ); + } ); - mw.user.getRights().done( function ( rights ) { - assert.deepEqual( rights, [ 'read', 'edit', 'createtalk' ], 'Result (promise)' ); - } ); + QUnit.test( 'getRights (Promise)', function ( assert ) { + this.server.respond( [ 200, { 'Content-Type': 'application/json' }, + '{ "query": { "userinfo": { "groups": [ "unused" ], "rights": [ "read", "edit", "createtalk" ] } } }' + ] ); - this.server.respondWith( /meta=userinfo/, function ( request ) { - request.respond( 200, { 'Content-Type': 'application/json' }, - '{ "query": { "userinfo": { "groups": [ "unused" ], "rights": [ "read", "edit", "createtalk" ] } } }' - ); + return mw.user.getRights().then( function ( rights ) { + assert.deepEqual( rights, [ 'read', 'edit', 'createtalk' ], 'Result (promise)' ); } ); - - this.server.respond(); } ); QUnit.test( 'generateRandomSessionId', function ( assert ) { diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js index ad6a0d0ab1..6b316e5558 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js @@ -80,6 +80,7 @@ }, teardown: function () { $.fn.updateTooltipAccessKeys.setTestMode( false ); + mw.util.resetOptionsForTest(); }, messages: { // Used by accessKeyLabel in test for addPortletLink @@ -114,7 +115,7 @@ // Distant future: no legacy fallbacks [ allNew, text, html5Encoded ] ].forEach( function ( testCase ) { - mw.config.set( 'wgFragmentMode', testCase[ 0 ] ); + mw.util.setOptionsForTest( { FragmentMode: testCase[ 0 ] } ); assert.strictEqual( util.escapeIdForAttribute( testCase[ 1 ] ), testCase[ 2 ] ); } ); @@ -141,7 +142,7 @@ // Distant future: no legacy fallbacks [ allNew, text, html5Encoded ] ].forEach( function ( testCase ) { - mw.config.set( 'wgFragmentMode', testCase[ 0 ] ); + mw.util.setOptionsForTest( { FragmentMode: testCase[ 0 ] } ); assert.strictEqual( util.escapeIdForLink( testCase[ 1 ] ), testCase[ 2 ] ); } ); @@ -150,7 +151,7 @@ QUnit.test( 'wikiUrlencode', function ( assert ) { assert.strictEqual( util.wikiUrlencode( 'Test:A & B/Here' ), 'Test:A_%26_B/Here' ); // See also wfUrlencodeTest.php#provideURLS - // eslint-disable-next-line jquery/no-each-util + // eslint-disable-next-line no-jquery/no-each-util $.each( { '+': '%2B', '&': '%26', @@ -210,22 +211,22 @@ href = util.getUrl( '#Fragment', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?action=edit#Fragment', 'empty title with query string and fragment' ); - mw.config.set( 'wgFragmentMode', [ 'legacy' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'legacy' ] } ); href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_.C3.84', 'title with query string, fragment, and special characters' ); - mw.config.set( 'wgFragmentMode', [ 'html5' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'html5' ] } ); href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_Ä', 'title with query string, fragment, and special characters' ); href = util.getUrl( 'Foo:%23#Fragment', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?title=Foo:%2523&action=edit#Fragment', 'title containing %23 (#), fragment, and a query string' ); - mw.config.set( 'wgFragmentMode', [ 'legacy' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'legacy' ] } ); href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?action=edit#.2B.26.3D:.3B.40.24-_..21.2A.2F.5B.5D.3C.3E.27.C2.A7', 'fragment with various characters' ); - mw.config.set( 'wgFragmentMode', [ 'html5' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'html5' ] } ); href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?action=edit#+&=:;@$-_.!*/[]<>\'§', 'fragment with various characters' ); } ); diff --git a/tests/selenium/.eslintrc.json b/tests/selenium/.eslintrc.json index 38cd062a90..dd766c85fb 100644 --- a/tests/selenium/.eslintrc.json +++ b/tests/selenium/.eslintrc.json @@ -7,7 +7,8 @@ "mocha": true }, "globals": { - "browser": false + "browser": false, + "mw": false }, "rules": { "no-console": 0 diff --git a/tests/selenium/pageobjects/history.page.js b/tests/selenium/pageobjects/history.page.js index acaf3ea0fa..da5e90961e 100644 --- a/tests/selenium/pageobjects/history.page.js +++ b/tests/selenium/pageobjects/history.page.js @@ -1,11 +1,43 @@ -const Page = require( 'wdio-mediawiki/Page' ); +const Page = require( 'wdio-mediawiki/Page' ), + Api = require( 'wdio-mediawiki/Api' ); class HistoryPage extends Page { + get heading() { return browser.element( '#firstHeading' ); } + get headingText() { return browser.getText( '#firstHeading' ); } get comment() { return browser.element( '#pagehistory .comment' ); } + get rollback() { return browser.element( '.mw-rollback-link' ); } + get rollbackLink() { return browser.element( '.mw-rollback-link a' ); } + get rollbackConfirmable() { return browser.element( '.mw-rollback-link .jquery-confirmable-text' ); } + get rollbackConfirmableYes() { return browser.element( '.mw-rollback-link .jquery-confirmable-button-yes' ); } + get rollbackConfirmableNo() { return browser.element( '.mw-rollback-link .jquery-confirmable-button-no' ); } + get rollbackNonJsConfirmable() { return browser.element( '.mw-htmlform .oo-ui-fieldsetLayout-header .oo-ui-labelElement-label' ); } + get rollbackNonJsConfirmableYes() { return browser.element( '.mw-htmlform .mw-htmlform-submit-buttons button' ); } open( title ) { super.openTitle( title, { action: 'history' } ); } + + vandalizePage( name, content ) { + let vandalUsername = 'Evil_' + browser.options.username; + + browser.call( function () { + return Api.edit( name, content ); + } ); + + browser.call( function () { + return Api.createAccount( + vandalUsername, browser.options.password + ); + } ); + + browser.call( function () { + Api.edit( + name, + 'Vandalized: ' + content, + vandalUsername + ); + } ); + } } module.exports = new HistoryPage(); diff --git a/tests/selenium/specs/page.js b/tests/selenium/specs/page.js index 3b2429808f..80e12cd56f 100644 --- a/tests/selenium/specs/page.js +++ b/tests/selenium/specs/page.js @@ -5,7 +5,7 @@ const assert = require( 'assert' ), EditPage = require( '../pageobjects/edit.page' ), HistoryPage = require( '../pageobjects/history.page' ), UndoPage = require( '../pageobjects/undo.page' ), - UserLoginPage = require( '../pageobjects/userlogin.page' ), + UserLoginPage = require( 'wdio-mediawiki/LoginPage' ), Util = require( 'wdio-mediawiki/Util' ); describe( 'Page', function () { @@ -91,7 +91,7 @@ describe( 'Page', function () { // check HistoryPage.open( name ); - assert.strictEqual( HistoryPage.comment.getText(), `(Created page with "${content}")` ); + assert.strictEqual( HistoryPage.comment.getText(), `Created or updated page with "${content}"` ); } ); it( 'should be deletable', function () { diff --git a/tests/selenium/specs/rollback.js b/tests/selenium/specs/rollback.js new file mode 100644 index 0000000000..805b793098 --- /dev/null +++ b/tests/selenium/specs/rollback.js @@ -0,0 +1,140 @@ +const assert = require( 'assert' ), + HistoryPage = require( '../pageobjects/history.page' ), + UserLoginPage = require( 'wdio-mediawiki/LoginPage' ), + Util = require( 'wdio-mediawiki/Util' ); + +describe( 'Rollback with confirmation', function () { + var content, + name; + + before( function () { + // disable VisualEditor welcome dialog + browser.deleteCookie(); + UserLoginPage.open(); + browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } ); + + // Enable rollback confirmation for admin user + // Requires user to log in again, handled by deleteCookie() call in beforeEach function + UserLoginPage.loginAdmin(); + + browser.pause( 300 ); + browser.execute( function () { + return ( new mw.Api() ).saveOption( + 'showrollbackconfirmation', + '1' + ); + } ); + } ); + + beforeEach( function () { + browser.deleteCookie(); + + content = Util.getTestString( 'beforeEach-content-' ); + name = Util.getTestString( 'BeforeEach-name-' ); + + HistoryPage.vandalizePage( name, content ); + + UserLoginPage.loginAdmin(); + HistoryPage.open( name ); + } ); + + it( 'should offer rollback options for admin users', function () { + assert.strictEqual( HistoryPage.rollback.getText(), 'rollback 1 edit' ); + + HistoryPage.rollback.click(); + + assert.strictEqual( HistoryPage.rollbackConfirmable.getText(), 'Please confirm:' ); + assert.strictEqual( HistoryPage.rollbackConfirmableYes.getText(), 'Rollback' ); + assert.strictEqual( HistoryPage.rollbackConfirmableNo.getText(), 'Cancel' ); + } ); + + it( 'should offer a way to cancel rollbacks', function () { + HistoryPage.rollback.click(); + browser.pause( 300 ); + HistoryPage.rollbackConfirmableNo.click(); + + browser.pause( 500 ); + + assert.strictEqual( HistoryPage.heading.getText(), 'Revision history of "' + name + '"' ); + } ); + + it( 'should perform rollbacks after confirming intention', function () { + HistoryPage.rollback.click(); + HistoryPage.rollbackConfirmableYes.click(); + + // waitUntil indirectly asserts that the content we are looking for is present + browser.waitUntil( function () { + return browser.getText( '#firstHeading' ) === 'Action complete'; + }, 5000, 'Expected rollback page to appear.' ); + } ); + + it( 'should verify rollbacks via GET requests are confirmed on a follow-up page', function () { + var rollbackActionUrl = HistoryPage.rollbackLink.getAttribute( 'href' ); + browser.url( rollbackActionUrl ); + + browser.waitUntil( function () { + return HistoryPage.rollbackNonJsConfirmable.getText() === 'Revert edits to this page?'; + }, 5000, 'Expected rollback confirmation page to appear for GET-based rollbacks.' ); + + HistoryPage.rollbackNonJsConfirmableYes.click(); + + browser.waitUntil( function () { + return browser.getText( '#firstHeading' ) === 'Action complete'; + }, 5000, 'Expected rollback page to appear.' ); + } ); + +} ); + +describe( 'Rollback without confirmation', function () { + var content, + name; + + before( function () { + // disable VisualEditor welcome dialog + browser.deleteCookie(); + UserLoginPage.open(); + browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } ); + + // Disable rollback confirmation for admin user + // Requires user to log in again, handled by deleteCookie() call in beforeEach function + UserLoginPage.loginAdmin(); + + browser.pause( 300 ); + browser.execute( function () { + return ( new mw.Api() ).saveOption( + 'showrollbackconfirmation', + '0' + ); + } ); + } ); + + beforeEach( function () { + browser.deleteCookie(); + + content = Util.getTestString( 'beforeEach-content-' ); + name = Util.getTestString( 'BeforeEach-name-' ); + + HistoryPage.vandalizePage( name, content ); + + UserLoginPage.loginAdmin(); + HistoryPage.open( name ); + } ); + + it( 'should perform rollback via POST request without asking the user to confirm', function () { + HistoryPage.rollback.click(); + + // waitUntil indirectly asserts that the content we are looking for is present + browser.waitUntil( function () { + return HistoryPage.headingText === 'Action complete'; + }, 5000, 'Expected rollback page to appear.' ); + } ); + + it( 'should perform rollback via GET request without asking the user to confirm', function () { + var rollbackActionUrl = HistoryPage.rollbackLink.getAttribute( 'href' ); + browser.url( rollbackActionUrl ); + + browser.waitUntil( function () { + return browser.getText( '#firstHeading' ) === 'Action complete'; + }, 5000, 'Expected rollback page to appear.' ); + } ); +} ); diff --git a/tests/selenium/wdio-mediawiki/Api.js b/tests/selenium/wdio-mediawiki/Api.js index f68fee95bf..7947ff504c 100644 --- a/tests/selenium/wdio-mediawiki/Api.js +++ b/tests/selenium/wdio-mediawiki/Api.js @@ -5,22 +5,31 @@ const MWBot = require( 'mwbot' ); module.exports = { /** * Shortcut for `MWBot#edit( .. )`. + * Default username, password and base URL is used unless specified * * @since 1.0.0 * @see * @param {string} title * @param {string} content + * @param {string} username - Optional + * @param {string} password - Optional + * @param {baseUrl} baseUrl - Optional * @return {Object} Promise for API action=edit response data. */ - edit( title, content ) { + edit( title, + content, + username = browser.options.username, + password = browser.options.password, + baseUrl = browser.options.baseUrl + ) { let bot = new MWBot(); return bot.loginGetEditToken( { - apiUrl: `${browser.options.baseUrl}/api.php`, - username: browser.options.username, - password: browser.options.password + apiUrl: `${baseUrl}/api.php`, + username: username, + password: password } ).then( function () { - return bot.edit( title, content, `Created page with "${content}"` ); + return bot.edit( title, content, `Created or updated page with "${content}"` ); } ); }, diff --git a/tests/selenium/wdio-mediawiki/RunJobs.js b/tests/selenium/wdio-mediawiki/RunJobs.js index 50ac601c5e..070ad56498 100644 --- a/tests/selenium/wdio-mediawiki/RunJobs.js +++ b/tests/selenium/wdio-mediawiki/RunJobs.js @@ -1,6 +1,43 @@ const MWBot = require( 'mwbot' ), Page = require( './Page' ), - FRONTPAGE_REQUESTS_MAX_RUNS = 10; // (arbitrary) safe-guard against endless execution + MAINPAGE_REQUESTS_MAX_RUNS = 10; // (arbitrary) safe-guard against endless execution + +function getJobCount() { + let bot = new MWBot( { + apiUrl: `${browser.options.baseUrl}/api.php` + } ); + return bot.request( { + action: 'query', + meta: 'siteinfo', + siprop: 'statistics' + } ).then( ( response ) => { + return response.query.statistics.jobs; + } ); +} + +function log( message ) { + process.stdout.write( `RunJobs ${message}\n` ); +} + +function runThroughMainPageRequests( runCount = 1 ) { + let page = new Page(); + log( `through requests to the main page (run ${runCount}).` ); + + page.openTitle( '' ); + + return getJobCount().then( ( jobCount ) => { + if ( jobCount === 0 ) { + log( 'found no more queued jobs.' ); + return; + } + log( `detected ${jobCount} more queued job(s).` ); + if ( runCount >= MAINPAGE_REQUESTS_MAX_RUNS ) { + log( 'stopping requests to the main page due to reached limit.' ); + return; + } + return runThroughMainPageRequests( ++runCount ); + } ); +} /** * Trigger the execution of jobs @@ -26,48 +63,9 @@ class RunJobs { static run() { browser.call( () => { - return this.runThroughFrontPageRequests(); + return runThroughMainPageRequests(); } ); } - - static getJobCount() { - let bot = new MWBot( { - apiUrl: `${browser.options.baseUrl}/api.php` - } ); - return new Promise( ( resolve ) => { - return bot.request( { - action: 'query', - meta: 'siteinfo', - siprop: 'statistics' - } ).then( ( response ) => { - resolve( response.query.statistics.jobs ); - } ); - } ); - } - - static runThroughFrontPageRequests( runCount = 1 ) { - let page = new Page(); - this.log( `through requests to the front page (run ${runCount}).` ); - - page.openTitle( '' ); - - return this.getJobCount().then( ( jobCount ) => { - if ( jobCount === 0 ) { - this.log( 'found no more queued jobs.' ); - return; - } - this.log( `detected ${jobCount} more queued job(s).` ); - if ( runCount >= FRONTPAGE_REQUESTS_MAX_RUNS ) { - this.log( 'stopping requests to the front page due to reached limit.' ); - return; - } - return this.runThroughFrontPageRequests( ++runCount ); - } ); - } - - static log( message ) { - process.stdout.write( `RunJobs ${message}\n` ); - } } module.exports = RunJobs; diff --git a/tests/selenium/wdio.conf.js b/tests/selenium/wdio.conf.js index 916ee74205..bdabdbfdb3 100644 --- a/tests/selenium/wdio.conf.js +++ b/tests/selenium/wdio.conf.js @@ -117,7 +117,8 @@ exports.config = { // Setting this enables automatic screenshots for when a browser command fails // It is also used by afterTest for capturig failed assertions. - screenshotPath: logPath, + // We disable it since we have our screenshot handler in the afterTest hook. + screenshotPath: null, // Default timeout for each waitFor* command. waitforTimeout: 10 * 1000, @@ -153,7 +154,8 @@ exports.config = { */ beforeTest: function ( test ) { if ( process.env.DISPLAY && process.env.DISPLAY.startsWith( ':' ) ) { - let videoPath = filePath( test, this.screenshotPath, 'mp4' ); + var logBuffer; + let videoPath = filePath( test, logPath, 'mp4' ); const { spawn } = require( 'child_process' ); ffmpeg = spawn( 'ffmpeg', [ '-f', 'x11grab', // grab the X11 display @@ -165,17 +167,29 @@ exports.config = { videoPath // output file ] ); + logBuffer = function ( buffer, prefix ) { + let lines = buffer.toString().trim().split( '\n' ); + lines.forEach( function ( line ) { + console.log( prefix + line ); + } ); + }; + ffmpeg.stdout.on( 'data', ( data ) => { - console.log( `ffmpeg stdout: ${data}` ); + logBuffer( data, 'ffmpeg stdout: ' ); } ); ffmpeg.stderr.on( 'data', ( data ) => { - console.log( `ffmpeg stderr: ${data}` ); + logBuffer( data, 'ffmpeg stderr: ' ); } ); - ffmpeg.on( 'close', ( code ) => { + ffmpeg.on( 'close', ( code, signal ) => { console.log( '\n\tVideo location:', videoPath, '\n' ); - console.log( `ffmpeg exited with code ${code}` ); + if ( code !== null ) { + console.log( `\tffmpeg exited with code ${code} ${videoPath}` ); + } + if ( signal !== null ) { + console.log( `\tffmpeg received signal ${signal} ${videoPath}` ); + } } ); } }, @@ -196,8 +210,8 @@ exports.config = { return; } // save screenshot - let screenshotPath = filePath( test, this.screenshotPath, 'png' ); - browser.saveScreenshot( screenshotPath ); - console.log( '\n\tScreenshot location:', screenshotPath, '\n' ); + let screenshotfile = filePath( test, logPath, 'png' ); + browser.saveScreenshot( screenshotfile ); + console.log( '\n\tScreenshot location:', screenshotfile, '\n' ); } };