diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6043363 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ + +### Perl ### +!Build/ +.last_cover_stats +/META.yml +/META.json +/MYMETA.* +*.o +*.pm.tdy +*.bs + +# ExtUtils::MakeMaker +/blib/ +/_eumm/ +/*.gz +/Makefile +/Makefile.old +/MANIFEST.bak +/pm_to_blib +/*.zip diff --git a/docker/functions.sh b/docker/functions.sh new file mode 100644 index 0000000..d164bba --- /dev/null +++ b/docker/functions.sh @@ -0,0 +1,106 @@ +# Shell functions for running docker containers -*- bash -*- + +startContainer() { + port=$1 + containerName=$2 + + docker run -d -p $port:80 -e LC_ALL=C.UTF-8 $containerName +} + +isContainerUp() { + container=$1 + + docker logs $container 2>&1 | grep -q apache2.-D.FOREGROUND + echo $? +} + +getContainerReady() { + port=$1 + containerName=$2 + + container=$(startContainer $port $containerName) + ready=$(isContainerUp $container) + + while [ "$ready" -ne 0 ] ; do + ready=$(isContainerUp $container) + + sleep 1 + done + echo $container +} + +setupMW() { + container=$1 + wikiadmin=$2 + wikipass=$3 + + docker exec $container php /var/www/html/maintenance/install.php \ + --dbtype=sqlite --dbpath=/tmp --pass=$wikipass \ + --skins=Vector --scriptpath= core $wikiadmin | \ + grep -q 'MediaWiki has been successfully installed' + if [ $? -ne 0 ]; then + echo Trouble installing MediaWiki in container! 1>&2 + teardownContainer $container + exit 2 + fi + docker exec $container sh -c \ + 'chown www-data /tmp/*.sqlite /var/www/html/images \ + /var/www/html/cache' + # Turn on uploads for the tests + docker exec $container sh -c \ + 'echo "\$wgEnableUploads = true;" >> \ + /var/www/html/LocalSettings.php' + # Allow i18n caching to disk + docker exec $container sh -c \ + 'echo "\$wgCacheDirectory = \"\$IP/cache\";" >> \ + /var/www/html/LocalSettings.php' + # Turn off most caching + docker exec $container sh -c \ + 'echo "\$wgMainCacheType = CACHE_NONE;" >> \ + /var/www/html/LocalSettings.php' + # Run a lot of jobs each time + docker exec $container sh -c \ + 'echo "\$wgRunJobsAsync = false;" >> \ + /var/www/html/LocalSettings.php' + docker exec $container sh -c \ + 'echo "\$wgJobRunRate = 10;" >> \ + /var/www/html/LocalSettings.php' + # Debug logs for troubleshooting + docker exec $container sh -c \ + 'echo "\$wgDebugLogFile = \"/tmp/debug.log\";" >> \ + /var/www/html/LocalSettings.php' +} + +teardownContainer() { + container=$1 + + result=$(docker kill $container) + if [ "$result" != "$container" ]; then + echo Some problem with the kill? + exit 3 + fi + + result=$(docker rm $container) + if [ "$result" != "$container" ]; then + echo Some problem with removal? + exit 4 + fi +} + +# From https://unix.stackexchange.com/a/358101 +getFreePort() { + netstat -aln | awk ' + $6 == "LISTEN" { + if ($4 ~ "[.:][0-9]+$") { + split($4, a, /[:.]/); + port = a[length(a)]; + p[port] = 1 + } + } + END { + for (i = 3000; i < 65000 && p[i]; i++){}; + if (i == 65000) {exit 1}; + print i + } +' +} diff --git a/docker/run-tests.sh b/docker/run-tests.sh new file mode 100755 index 0000000..bc5145f --- /dev/null +++ b/docker/run-tests.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +. docker/functions.sh + +WIKI_DIAGNOSTICS=${WIKI_DIAGNOSTICS:-0} +export WIKI_DIAGNOSTICS + +WIKI_PASS=${WIKI_PASS:-pass123456} +export WIKI_PASS + +WIKI_ADMIN=${WIKI_ADMIN:-admin} +export WIKI_ADMIN + +WIKI_PORT=${WIKI_PORT:-$(getFreePort)} + +WIKI_API_URL=${WIKI_API_URL:-http://localhost:$WIKI_PORT/api.php} +export WIKI_API_URL + +# 1.27 does not work due to a change made in install.php +WIKI_IMAGE=${WIKI_IMAGE:-mediawiki:latest} + +if [ -z "$WIKI_PORT" ]; then + echo Could not find a free port to use. + exit 10 +fi + +echo -n "Preparing container to listen on $WIKI_PORT... " +container=$(getContainerReady $WIKI_PORT $WIKI_IMAGE) +echo up as $container. + +echo -n "Setting up SQLite-based MW... " +setupMW $container $WIKI_ADMIN $WIKI_PASS +echo done. + +echo Running tests... +make; make test || echo FAILURES, but killing container anyway + +echo -n "Tearing down container... " +teardownContainer $container +echo done. diff --git a/lib/MediaWiki/API.pm b/lib/MediaWiki/API.pm index 60d4e9a..d3b4fa6 100644 --- a/lib/MediaWiki/API.pm +++ b/lib/MediaWiki/API.pm @@ -10,9 +10,9 @@ use URI::Escape; use Encode; use JSON; use Carp; +use Data::Dumper; # just for debugging the module -# use Data::Dumper; # use Devel::Peek; use constant { @@ -118,6 +118,8 @@ Configuration options are =item * no_proxy = Boolean; Set to 1 to Disable use of any proxy set in the environment. Note by default if you have proxy environment variables set, then the module will attempt to use them. This feature was added at version 0.29. Versions below this ignore any proxy settings, but you can set this yourself by doing MediaWiki::API->{ua}->env_proxy() after creating a new instance of the API class. More information about env_proxy can be found at http://search.cpan.org/~gaas/libwww-perl-5.834/lib/LWP/UserAgent.pm#Proxy_attributes +=item * diagnostics = Boolean; Diagnostics will be printed to STDERR + =back An example for the on_error configuration could be something like: @@ -197,6 +199,7 @@ sub new { $self->{error}->{details} = ''; $self->{error}->{stacktrace} = ''; + $self->{diagnostics} = $config->{diagnostics}; bless ($self, $class); return $self; } @@ -221,9 +224,26 @@ sub _get_config_defaults { 'paraminfo' => 1 }; + $config{diagnostics} = 0; + return \%config; } +sub _diag { + my ( $self, $msg ) = @_; + + if ( $self->{diagnostics} ) { + for ($msg) { + s{\n}{\n# }gsmx; + s{(Doing|Returning).[\$]VAR1.=.}{$1 }smx; + s{;$}{}smx; + } + + warn "# $msg\n"; + } + return; +} + =head2 MediaWiki::API->login( $query_hashref ) Logs in to a MediaWiki. Parameters are those used by the MediaWiki API (http://www.mediawiki.org/wiki/API:Login). Returns a hashref with some login details, or undef on login failure. If Mediawiki sends requests a LoginToken the login is attempted again, but with the token sent from the initial login. Errors are stored in MediaWiki::API->{error}->{code} and MediaWiki::API->{error}->{details}. @@ -344,6 +364,7 @@ sub api { my $retries = $self->{config}->{retries}; my $maxlagretries = 1; + $self->_diag( 'Doing ' . Dumper($query) ); $self->_encode_hashref_utf8($query, $options->{skip_encoding}); $query->{maxlag} = $self->{config}->{max_lag} if defined $self->{config}->{max_lag}; @@ -364,6 +385,7 @@ sub api { # if we are already retrying, then wait the specified delay if ( $try > 0 ) { + $self->_diag('Sleeping before retry.'); sleep $self->{config}->{retry_delay}; } @@ -371,9 +393,13 @@ sub api { my %headers; # if we are using the get method ($querystring is set above) if ( $querystring ) { + $self->_diag("GETting $querystring"); $response = $self->{ua}->get( $self->{config}->{api_url} . $querystring, %headers ); } else { - $headers{'content-type'} = 'form-data' if $query->{action} eq 'upload' || $query->{action} eq 'import'; + if ( $query->{action} eq 'upload' || $query->{action} eq 'import' ) { + $self->_diag("'{$query->{action}}' uses form-data header"); + $headers{'content-type'} = 'form-data'; + } $response = $self->{ua}->post( $self->{config}->{api_url}, $query, %headers ); } $self->{response} = $response; @@ -381,15 +407,18 @@ sub api { # if the request was successful then check the returned content and decode. if ( $response->is_success ) { + $self->_diag('Successful request.'); my $decontent = $response->decoded_content( charset => 'none' ); if ( ! defined $decontent ) { + $self->_diag('Unable to decode HTTP content body'); return $self->_error(ERR_HTTP,"Unable to decode content returned by $self->{config}->{api_url} - Unknown content encoding?") if ( $try == $retries ); next; - } + } if ( length $decontent == 0 ) { + $self->_diag('Zero-length content.'); return $self->_error(ERR_HTTP,"$self->{config}->{api_url} returned a zero length string") if ( $try == $retries ); next; @@ -401,10 +430,14 @@ sub api { }; if ( $@) { + $self->_diag("Failed to decode content: $decontent"); # an error occurred, so we check if we need to retry and continue my $error = $@; - return $self->_error(ERR_HTTP,"Failed to decode JSON returned by $self->{config}->{api_url}\nDecoding Error:\n$error\nReturned Data:\n$decontent") - if ( $try == $retries ); + if ( $try == $retries ) { + $self->_diag( + 'Retry limit reached without a successful response.'); + return $self->_error(ERR_HTTP,"Failed to decode JSON returned by $self->{config}->{api_url}\nDecoding Error:\n$error\nReturned Data:\n$decontent") + } next; } else { # no error so we want out of the retry loop @@ -417,23 +450,28 @@ sub api { return $self->_error(ERR_HTTP, $response->status_line . " : error occurred when accessing $self->{config}->{api_url} after " . ($try+1) . " attempt(s)") if ( $try == $retries ); next; - } + } } - return $self->_error(ERR_API,"API has returned an empty array reference. Please check your parameters") if ( ref($ref) eq 'ARRAY' && scalar @{$ref} == 0); + if ( ref($ref) eq 'ARRAY' && scalar @{$ref} == 0) { + $self->_diag('Empty array response.'); + return $self->_error(ERR_API,"API has returned an empty array reference. Please check your parameters") + } # check lag and wait if (ref($ref) eq 'HASH' && exists $ref->{error} && $ref->{error}->{code} eq 'maxlag' ) { - if ($maxlagretries == $self->{config}->{max_lag_retries}) { - return $self->_error(ERR_API,"Server has reported lag above the configured max_lag value of " . $self->{config}->{max_lag} . " value after " . $self->{config}->{max_lag_retries} . " attempt(s). Last reported lag was - ". $ref->{'error'}->{'info'}) - } else { - sleep $self->{config}->{max_lag_delay}; - $maxlagretries++ if $maxlagretries < $self->{config}->{max_lag_retries}; - # redo the request - next; - } + if ($maxlagretries == $self->{config}->{max_lag_retries}) { + $self->_diag('Reached limit for too much lag time'); + return $self->_error(ERR_API,"Server has reported lag above the configured max_lag value of " . $self->{config}->{max_lag} . " value after " . $self->{config}->{max_lag_retries} . " attempt(s). Last reported lag was - ". $ref->{'error'}->{'info'}) + } else { + sleep $self->{config}->{max_lag_delay}; + $maxlagretries++ if $maxlagretries < $self->{config}->{max_lag_retries}; + $self->_diag('Too much lag time, retrying'); + # redo the request + next; + } } # if we got this far, then we have a hashref from the api and we want out of the while loop @@ -441,8 +479,11 @@ sub api { } - return $self->_error(ERR_API,$ref->{error}->{code} . ": " . $ref->{error}->{info} ) if ( ref($ref) eq 'HASH' && exists $ref->{error} ); - + if ( ref($ref) eq 'HASH' && exists $ref->{error} ) { + $self->_diag( 'Error from server: ' . $ref->{error}->{info} ); + } else { + $self->_diag( 'Returning ' . Dumper($ref) ); + } return $ref; } diff --git a/t/10-api.t b/t/10-api.t index 9686371..ee164b0 100644 --- a/t/10-api.t +++ b/t/10-api.t @@ -1,100 +1,192 @@ -#!perl -T +#!perl use strict; use warnings; -use Test::More; +use Test::Most; use LWP::UserAgent; +use Readonly; +use Data::Dumper; + +Readonly my $TIMEOUT => 10; sub get_url { - my $url = shift; - my $ua = LWP::UserAgent->new; - $ua->timeout(10); - $ua->env_proxy; - my $response = $ua->get($url); - return $response; + my $url = shift; + my $ua = LWP::UserAgent->new; + $ua->timeout($TIMEOUT); + $ua->env_proxy; + my $response = $ua->get($url); + return $response; } my $api_url = 'http://testwiki.exotica.org.uk/mediawiki/api.php'; +my $empty_msg = 'The file you submitted was empty'; + +if ( $ENV{WIKI_API_URL} ) { + $api_url = $ENV{WIKI_API_URL}; + $empty_msg = 'The file you submitted was empty.'; +} + +my ($site_info) = $api_url =~ m{( [^:]+ [:]// [^/]+ ) /}smx; + +diag("Using '$api_url' for the tests..."); my $response = get_url($api_url); -if ($response->is_success) { - plan tests => 11; -} else { - plan skip_all => "Can't access $api_url to run tests"; +if ( $response->is_success ) { + plan tests => 20; +} +else { + plan skip_all => "Can't access $api_url to run tests"; } -use_ok( 'MediaWiki::API' ); -my $mw = MediaWiki::API->new( { api_url => $api_url } ); +use_ok('MediaWiki::API'); +my $mw = MediaWiki::API->new( + { + api_url => $api_url, + diagnostics => $ENV{WIKI_DIAGNOSTICS} + } +); isa_ok( $mw, 'MediaWiki::API' ); -$mw->{config}->{upload_url} = 'http://testwiki.exotica.org.uk/wiki/Special:Upload'; my $ref; -ok ( $ref = $mw->api( { - action => 'query', - meta => 'siteinfo' - } ), - '->api siteinfo call' - ); - -is ( $ref->{query}->{general}->{server}, 'http://testwiki.exotica.org.uk', '->api siteinfo server' ); - -ok ( $mw->api( { - action => 'query', - list => 'allcategories' - } ), - '->list allcategories' - ); - -my $time = time; +ok( + $ref = $mw->api( + { + action => 'query', + meta => 'siteinfo' + } + ), + '->api siteinfo call' +); + +is( $ref->{query}->{general}->{server}, $site_info, '->api siteinfo server' ); + +ok( + $ref = $mw->api( + { + action => 'query', + list => 'allcategories' + } + ), + '->list allcategories' +); + +my $time = time; my $title = 'apitest/' . $time; -my $content = "* Version: $MediaWiki::API::VERSION\n\nthe quick brown fox jumps over the lazy dog"; -ok ( $mw->edit( { - action => 'edit', - title => $title, - text => $content, - summary => 'MediaWiki::API Test suite - edit page', - bot => 1 - } ), - '->edit ' . $title - ); - -ok ( $ref = $mw->get_page( { title => $title } ), "->get_page $title call" ); - -is ( $ref->{'*'}, $content, "->get_page $title content" ); +my $content = +"* Version: $MediaWiki::API::VERSION\n\nthe quick brown fox jumps over the lazy dog"; +ok( + $ref = $mw->edit( { + action => 'edit', + title => $title, + text => $content, + summary => 'MediaWiki::API Test suite - edit page', + bot => 1 + } + ), + '->edit ' . $title +); + +ok( $ref = $mw->get_page( { title => $title } ), "->get_page $title call" ); + +is( $ref->{q{*}}, $content, "->get_page $title content" ); + +SKIP: { + if ( !$ENV{WIKI_API_URL} ) { + skip "Traditional test wiki allows anons", 4 + }; +# The move won't succeed if we don't log in + ok( !$mw->edit( + { + action => 'move', + from => $title, + to => $title . '-move', + summary => 'MediaWiki::API Test suite - move page', + bot => 1 + } + ), + '->edit action=move ' . $title + ); + cmp_ok( $mw->{error}->{code}, + q{==}, 5, 'Anon failed to move page' ); + + bail_on_fail(); + ok( $ref = $mw->login( {lgname => $ENV{WIKI_ADMIN}, lgpassword => $ENV{WIKI_PASS}} ), '->login' ); + ok( !exists( $ref->{error}->{code} ), 'No errors during login' ); + restore_fail(); +} -ok ( $mw->edit( { - action => 'move', - from => $title, - to => $title . '-moved', - summary => 'MediaWiki::API Test suite - move page', - bot => 1 - } ), - '->edit action=move ' . $title - ); +# The move will succeed now +ok( + $ref = $mw->edit( + { + action => 'move', + from => $title, + to => $title . '-moved', + bot => 1, + summary => 'MediaWiki::API Test suite - move page', + bot => 1 + } + ), + '->edit logged in action=move ' . $title +); $title = $title . '-moved'; -ok ( $mw->edit( { - action => 'delete', - title => $title, - summary => 'MediaWiki::API Test suite - delete page', - bot => 1 - } ), - '->edit action=delete ' . $title - ); +ok( + $ref = $mw->edit( + { + title => $title, + action => 'delete', + summary => 'MediaWiki::API Test suite - delete page', + bot => 1 + } + ), + '->edit action=delete ' . $title +); + +delete $mw->{error}->{code}; +delete $mw->{error}->{details}; $title = "apitest - $time.png"; -ok ( $mw->edit( { - action => 'upload', - filename => $title, - comment => 'MediaWiki::API Test suite - upload image', - file => [ 't/testimage.png'], - ignorewarnings => 1, - bot => 1 - } ), - "->edit action=upload $title" +ok( + $ref = $mw->upload( + { + title => $title, + comment => 'MediaWiki::API Test suite - upload image', + file => undef, + ignorewarnings => 1, + bot => 1 + } + ), + "->edit action=upload $title" ); - +cmp_ok( $ref->{error}->{info}, 'eq', + $empty_msg, + 'Error from empty file.' ); +cmp_ok( $ref->{error}->{code}, 'eq', 'empty-file', + 'Error when trying to upload' ); + +delete $mw->{error}->{code}; +delete $mw->{error}->{details}; + +ok( + $ref = $mw->upload( + { + title => $title, + comment => 'MediaWiki::API Test suite - upload image', + file => [ 't/testimage.png'], + ignorewarnings => 1, + bot => 1 + } + ), + "->edit action=upload $title" +); + +cmp_ok( $ref->{error}->{info}, 'eq', + $empty_msg, + 'Error from empty file.' ); +cmp_ok( $ref->{error}->{code}, 'eq', 'empty-file', + 'Error when trying to upload' ); done_testing();