From 06435e6b71c60b020e4294ee501ce8fab2a1bbf8 Mon Sep 17 00:00:00 2001 From: Vadim Belman Date: Thu, 25 Aug 2016 20:46:27 -0400 Subject: [PATCH] Item13897: PlackTestCase is finished. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is another and hopefully the last megacommit to this branch. It may have impact on overall code stability of this branch. I'm sorry for that but to write those three words in the commit subject took a bit more than just few changes. Next time a branch will be used. With this commit I consider PlackTestCase ready for use. It has all the basic functionality needed to test the application with Plack::Test and the same time benefit from the old UnitTestContrib code. No documentation yet but it come in a day or two. - Added PlackPostTests as sample test suite for PlackTestCase. - Extracted all code which can serve for both FoswikiTestCase/FoswikiFnTestCase and PlackTestCase into a new role FoswikiTestRole. This also includes splitting of test initialization code into several `setupSomething()' methods. - Renamed pushApp/popApp test case methods to saveState/restoreState to correspond better with their purpose. - Step-by-step replacement of Foswiki::Func::apiFunc() to $Foswiki::app->apiFunc() or $this->app->apiFunc() wherever possible. - Step-by-step switching from 'use Moo' to 'use Foswiki::Class'. - Dropping support for old-style positional parameters for object constructors. - Added new exception Foswiki::Exception::FileOp for failed file operations. It automates use of $!. - Unit::TestApp now registers callbacks as early as possible – before Foswiki::App::cfg attribute get initialized. This is to make it possible for tests to manipulate with LSC before application is completely initialized. - Continue replacing $Foswiki::cfg with app->cfg->data. - Added isGuest() method to Foswiki::Users. - Added parameter `app' to Foswiki::Class. It automates applying of Foswiki::AppObject role to a class. - Fixed a strange bug where Foswiki::App::user attribute read was overoptimized for reading when passed as a method argument. The optimizations resulted in stack corruption if the attribute has been changed before completing the method. So, it is now mandatory for the attribute to be lazy and have a builder. - `postConfig' callback is now raised by Foswiki::App constructor right after checking the configuration attribute status and before any other initialization is done. - Fixed exception handling in Foswiki::UI::Register. - Foswiki::Aux::Localize::setLocalizeFlags is now returning key/value pairs list instead of hashref. This is to simplify it's use with inheritance. - Fixed test case registerUser() method failing to detect errors generated by Foswiki::UI::Register. --- .../lib/Foswiki/Users/TopicUserMapping.pm | 156 ++-- UnitTestContrib/lib/Unit/FoswikiTestRole.pm | 763 ++++++++++++++++++ UnitTestContrib/lib/Unit/PlackTestCase.pm | 339 ++++++-- UnitTestContrib/lib/Unit/TestApp.pm | 9 +- UnitTestContrib/lib/Unit/TestRunner.pm | 8 +- .../test/unit/ConfigureTestCase.pm | 2 - UnitTestContrib/test/unit/Fn_SEARCH.pm | 2 +- UnitTestContrib/test/unit/FormattingTests.pm | 2 - .../test/unit/FoswikiFnTestCase.pm | 144 +--- UnitTestContrib/test/unit/FoswikiTestCase.pm | 318 +------- UnitTestContrib/test/unit/FuncTests.pm | 5 +- UnitTestContrib/test/unit/FuncUsersTests.pm | 6 +- UnitTestContrib/test/unit/ManageDotPmTests.pm | 234 +++--- UnitTestContrib/test/unit/PasswordTests.pm | 4 - UnitTestContrib/test/unit/PlackPostTests.pm | 181 +++++ UnitTestContrib/test/unit/PlackViewTests.pm | 13 +- .../test/unit/PluginHandlerTests.pm | 4 +- core/lib/Foswiki/App.pm | 63 +- core/lib/Foswiki/AppObject.pm | 2 + core/lib/Foswiki/Aux/Localize.pm | 9 +- core/lib/Foswiki/Class.pm | 68 +- core/lib/Foswiki/Config.pm | 10 +- core/lib/Foswiki/Exception.pm | 71 +- core/lib/Foswiki/Meta.pm | 84 +- core/lib/Foswiki/MetaCache.pm | 7 +- core/lib/Foswiki/Net.pm | 2 +- core/lib/Foswiki/Object.pm | 20 +- core/lib/Foswiki/OopsException.pm | 4 +- core/lib/Foswiki/Plugin.pm | 6 +- core/lib/Foswiki/Request/Attachment.pm | 1 - core/lib/Foswiki/Search.pm | 9 +- core/lib/Foswiki/Store.pm | 6 +- core/lib/Foswiki/UI.pm | 11 +- core/lib/Foswiki/UI/Register.pm | 114 ++- core/lib/Foswiki/Users.pm | 32 +- 35 files changed, 1782 insertions(+), 927 deletions(-) create mode 100644 UnitTestContrib/lib/Unit/FoswikiTestRole.pm create mode 100644 UnitTestContrib/test/unit/PlackPostTests.pm diff --git a/TopicUserMappingContrib/lib/Foswiki/Users/TopicUserMapping.pm b/TopicUserMappingContrib/lib/Foswiki/Users/TopicUserMapping.pm index 736115df13..f4e1ff4c45 100755 --- a/TopicUserMappingContrib/lib/Foswiki/Users/TopicUserMapping.pm +++ b/TopicUserMappingContrib/lib/Foswiki/Users/TopicUserMapping.pm @@ -33,16 +33,15 @@ use Try::Tiny; use Foswiki::ListIterator (); use Foswiki::Func (); -use Moo; -use namespace::clean; +use Foswiki::Class qw(app); extends qw(Foswiki::Object); -with qw(Foswiki::AppObject Foswiki::UserMapping); +with qw(Foswiki::UserMapping); has passwords => ( is => 'ro', lazy => 1, default => sub { - my $implPasswordManager = $Foswiki::cfg{PasswordManager}; + my $implPasswordManager = $_[0]->app->cfg->data->{PasswordManager}; $implPasswordManager = 'Foswiki::Users::Password' if ( $implPasswordManager eq 'none' ); return $_[0]->create($implPasswordManager); @@ -235,8 +234,8 @@ sub getLoginName { sub _userReallyExists { my ( $this, $login ) = @_; - if ( $Foswiki::cfg{Register}{AllowLoginName} - || $Foswiki::cfg{PasswordManager} eq 'none' ) + if ( $this->app->cfg->data->{Register}{AllowLoginName} + || $this->app->cfg->data->{PasswordManager} eq 'none' ) { # need to use the WikiUsers file @@ -348,12 +347,14 @@ and vice-versa. The default implementation uses a special topic called sub _maintainUsersTopic { my ( $this, $action, $login, $wikiname ) = @_; + my $cfgData = $this->app->cfg->data; + my $usersTopicObject; if ( $this->app->store->topicExists( - $Foswiki::cfg{UsersWebName}, - $Foswiki::cfg{UsersTopicName} + $cfgData->{UsersWebName}, + $cfgData->{UsersTopicName} ) ) { @@ -361,8 +362,8 @@ sub _maintainUsersTopic { # Load existing users topic $usersTopicObject = $this->create( 'Foswiki::Meta', - web => $Foswiki::cfg{UsersWebName}, - topic => $Foswiki::cfg{UsersTopicName}, + web => $cfgData->{UsersWebName}, + topic => $cfgData->{UsersTopicName}, ); } else { @@ -371,18 +372,18 @@ sub _maintainUsersTopic { # Construct a new users topic from the template my $templateTopicObject = - Foswiki::Meta->load( $this->app, $Foswiki::cfg{SystemWebName}, + Foswiki::Meta->load( $this->app, $cfgData->{SystemWebName}, 'UsersTemplate' ); $usersTopicObject = $this->create( 'Foswiki::Meta', - web => $Foswiki::cfg{UsersWebName}, - topic => $Foswiki::cfg{UsersTopicName}, + web => $cfgData->{UsersWebName}, + topic => $cfgData->{UsersTopicName}, text => $templateTopicObject->text() ); $usersTopicObject->copyFrom($templateTopicObject); $usersTopicObject->put( "TOPICPARENT", - { name => $Foswiki::cfg{HomeTopicName} } ); + { name => $cfgData->{HomeTopicName} } ); } my $entry = " * $wikiname - "; @@ -390,7 +391,7 @@ sub _maintainUsersTopic { require Foswiki::Time; my $today = - Foswiki::Time::formatTime( time(), $Foswiki::cfg{DefaultDateFormat}, + Foswiki::Time::formatTime( time(), $cfgData->{DefaultDateFormat}, 'gmtime' ); my $user; @@ -418,7 +419,7 @@ sub _maintainUsersTopic { m/^\s+\*\s($Foswiki::regex{webNameRegex}\.)?($Foswiki::regex{wikiWordRegex})\s*(?:-\s*\w+\s*)?-\s*(.*)/ ) { - $web = $1 || $Foswiki::cfg{UsersWebName}; + $web = $1 || $cfgData->{UsersWebName}; $name = $2; $odate = $3; @@ -481,7 +482,7 @@ m/^\s+\*\s($Foswiki::regex{webNameRegex}\.)?($Foswiki::regex{wikiWordRegex})\s*( # SMELL: why is this Admin and not the RegoAgent?? $this->app->users->getCanonicalUserID( - $Foswiki::cfg{AdminUserLogin} + $cfgData->{AdminUserLogin} ) ); } @@ -550,12 +551,13 @@ If there is no matching WikiName or LoginName, it returns undef. sub getWikiName { my ( $this, $cUID ) = @_; my $mapping_id = $this->mapping_id; + my $cfgData = $this->app->cfg->data; ASSERT($cUID) if DEBUG; ASSERT( $cUID =~ m/^$mapping_id/ ) if DEBUG; my $wikiname; - if ( $Foswiki::cfg{Register}{AllowLoginName} ) { + if ( $cfgData->{Register}{AllowLoginName} ) { $this->_loadMapping(); $wikiname = $this->U2W->{$cUID}; } @@ -569,7 +571,7 @@ sub getWikiName { if ($wikiname) { # sanitise the generated WikiName - $wikiname =~ s/$Foswiki::cfg{NameFilter}//g; + $wikiname =~ s/$cfgData->{NameFilter}//g; } } @@ -589,13 +591,15 @@ sub userExists { my ( $this, $cUID ) = @_; ASSERT($cUID) if DEBUG; + my $cfgData = $this->app->cfg->data; + # Do this to avoid a password manager lookup return 1 if $cUID eq $this->app->user; my $loginName = $this->getLoginName($cUID); return 0 unless defined($loginName); - return 1 if ( $loginName eq $Foswiki::cfg{DefaultUserLogin} ); + return 1 if ( $loginName eq $cfgData->{DefaultUserLogin} ); # Foswiki allows *groups* to log in return 1 if ( $this->isGroup($loginName) ); @@ -605,7 +609,7 @@ sub userExists { if ( $this->passwords->canFetchUsers() && $this->passwords->fetchPass($loginName) ); - unless ( $Foswiki::cfg{Register}{AllowLoginName} + unless ( $cfgData->{Register}{AllowLoginName} && $this->passwords->canFetchUsers() ) { @@ -613,11 +617,7 @@ sub userExists { #and if AllowLoginName is also off, then the only way to know if #the user has registered is to test for user topic? my $wikiname = $this->app->users->getWikiName($cUID); - if ( - Foswiki::Func::topicExists( - $Foswiki::cfg{UsersWebName}, $wikiname - ) - ) + if ( Foswiki::Func::topicExists( $cfgData->{UsersWebName}, $wikiname ) ) { return 1; } @@ -669,6 +669,8 @@ my %expanding; # Prevents loops in nested groups sub eachGroupMember { my ( $this, $group, $options ) = @_; + my $cfgData = $this->app->cfg->data; + my $expand = $options->{expand}; if ( Scalar::Util::tainted($group) ) { @@ -712,14 +714,13 @@ sub eachGroupMember { } if ( !$expanding{$group} - && $app->store->topicExists( $Foswiki::cfg{UsersWebName}, $group ) ) + && $app->store->topicExists( $cfgData->{UsersWebName}, $group ) ) { $expanding{$group} = 1; # print "Expanding $group \n"; my $groupTopicObject = - Foswiki::Meta->load( $this->app, $Foswiki::cfg{UsersWebName}, - $group ); + Foswiki::Meta->load( $this->app, $cfgData->{UsersWebName}, $group ); if ( !$expand ) { $singleGroupMembers = @@ -757,8 +758,10 @@ See baseclass for documentation sub isGroup { my ( $this, $user ) = @_; + my $cfgData = $this->app->cfg->data; + # Groups have the same username as wikiname as canonical name - return 1 if $user eq $Foswiki::cfg{SuperAdminGroup}; + return 1 if $user eq $cfgData->{SuperAdminGroup}; return 0 unless ( $user =~ m/Group$/ ); @@ -826,14 +829,16 @@ sub groupAllowsView { my $user = $this->app->user; return 1 if $this->app->users->isAdmin($user); + my $cfgData = $this->app->cfg->data; + $Group = Foswiki::Sandbox::untaint( $Group, \&Foswiki::Sandbox::validateTopicName ); my ( $groupWeb, $groupName ) = - $this->app->request->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, + $this->app->request->normalizeWebTopicName( $cfgData->{UsersWebName}, $Group ); # If a Group or User topic normalized somewhere else, doesn't make sense, so ignore the Webname - $groupWeb = $Foswiki::cfg{UsersWebName}; + $groupWeb = $cfgData->{UsersWebName}; $groupName = undef if ( not $this->app->store->topicExists( $groupWeb, $groupName ) ); @@ -858,10 +863,12 @@ sub groupAllowsChange { my $user = shift; ASSERT( defined $user ) if DEBUG; + my $cfgData = $this->app->cfg->data; + $Group = Foswiki::Sandbox::untaint( $Group, \&Foswiki::Sandbox::validateTopicName ); my ( $groupWeb, $groupName ) = - $this->app->request->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, + $this->app->request->normalizeWebTopicName( $cfgData->{UsersWebName}, $Group ); # SMELL: Should NobodyGroup be configurable? @@ -869,7 +876,7 @@ sub groupAllowsChange { return 1 if $this->app->users->isAdmin($user); # If a Group or User topic normalized somewhere else, doesn't make sense, so ignore the Webname - $groupWeb = $Foswiki::cfg{UsersWebName}; + $groupWeb = $cfgData->{UsersWebName}; $groupName = undef if ( not $this->app->store->topicExists( $groupWeb, $groupName ) ); @@ -890,10 +897,11 @@ cuid be a groupname which is added like it was an unknown user sub addUserToGroup { my ( $this, $cuid, $Group, $create ) = @_; + my $cfgData = $this->app->cfg->data; $Group = Foswiki::Sandbox::untaint( $Group, \&Foswiki::Sandbox::validateTopicName ); my ( $groupWeb, $groupName ) = - $this->app->request->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, + $this->app->request->normalizeWebTopicName( $cfgData->{UsersWebName}, $Group ); Foswki::Exception->throw( text => @@ -937,7 +945,7 @@ sub addUserToGroup { my @l; foreach my $ident ( split( /[\,\s]+/, $membersString ) ) { - $ident =~ s/^($Foswiki::cfg{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//; + $ident =~ s/^($cfgData->{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//; push( @l, $ident ) if $ident; } $membersString = join( ', ', @l ); @@ -1006,7 +1014,7 @@ sub addUserToGroup { ); # reparse groups brute force :/ - _getListOfGroups( $this, 1 ) if ($create); + $this->_getListOfGroups(1) if ($create); return 1; } @@ -1087,10 +1095,11 @@ sub _writeGroupTopic { #TODO: should also consider securing the new topic? my $user = $this->app->user; $groupTopicObject->saveAs( - web => $groupWeb, - topic => $groupName, - author => $user, - forcenewrevision => ( $groupName eq $Foswiki::cfg{SuperAdminGroup} ) + web => $groupWeb, + topic => $groupName, + author => $user, + forcenewrevision => + ( $groupName eq $this->app->cfg->data->{SuperAdminGroup} ) ? 1 : 0 ); @@ -1105,10 +1114,13 @@ sub _writeGroupTopic { sub removeUserFromGroup { my ( $this, $cuid, $groupName ) = @_; + + my $cfgData = $this->app->cfg->data; + $groupName = Foswiki::Sandbox::untaint( $groupName, \&Foswiki::Sandbox::validateTopicName ); my ( $groupWeb, $groupTopic ) = - $this->app->request->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, + $this->app->request->normalizeWebTopicName( $cfgData->{UsersWebName}, $groupName ); Foswiki::Exception->throw( @@ -1120,12 +1132,10 @@ sub removeUserFromGroup { Foswiki::Exception->throw( text => $this->app->i18n->maketext( '[_1] cannot be removed from [_2]', - ( - $Foswiki::cfg{AdminUserWikiName}, $Foswiki::cfg{SuperAdminGroup} - ) + ( $cfgData->{AdminUserWikiName}, $cfgData->{SuperAdminGroup} ) ) ) - if ( $groupName eq "$Foswiki::cfg{SuperAdminGroup}" + if ( $groupName eq $cfgData->{SuperAdminGroup} && $cuid eq 'BaseUserMapping_333' ); my $user = $this->app->user; @@ -1135,7 +1145,7 @@ sub removeUserFromGroup { $usersObj->isGroup($groupName) and ( $this->app->store->topicExists( - $Foswiki::cfg{UsersWebName}, $groupName + $cfgData->{UsersWebName}, $groupName ) ) ) @@ -1152,7 +1162,7 @@ sub removeUserFromGroup { ); } my $groupTopicObject = - Foswiki::Meta->load( $this->app, $Foswiki::cfg{UsersWebName}, + Foswiki::Meta->load( $this->app, $cfgData->{UsersWebName}, $groupName ); if ( !$groupTopicObject->haveAccess( 'CHANGE', $user ) ) { @@ -1170,7 +1180,7 @@ sub removeUserFromGroup { my $membersString = $groupTopicObject->getPreference('GROUP'); my @l; foreach my $ident ( split( /[\,\s]+/, $membersString ) ) { - $ident =~ s/^($Foswiki::cfg{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//; + $ident =~ s/^($cfgData->{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//; next if ( $ident eq $WikiName ); next if ( $ident eq $LoginName ); next if ( $ident eq $cuid ); @@ -1237,11 +1247,11 @@ sub isAdmin { my $isAdmin = 0; # TODO: this might not apply now that we have BaseUserMapping - test - if ( $cUID eq $Foswiki::cfg{SuperAdminGroup} ) { + if ( $cUID eq $this->app->cfg->data->{SuperAdminGroup} ) { $isAdmin = 1; } else { - my $sag = $Foswiki::cfg{SuperAdminGroup}; + my $sag = $this->app->cfg->data->{SuperAdminGroup}; $isAdmin = $this->isInGroup( $cUID, $sag ); } @@ -1264,7 +1274,7 @@ sub findUserByEmail { my ( $this, $email ) = @_; ASSERT($email) if DEBUG; my @users; - if ( !$Foswiki::cfg{TopicUserMapping}{ForceManageEmails} + if ( !$this->app->cfg->data->{TopicUserMapping}{ForceManageEmails} && $this->passwords->isManagingEmails() ) { my $logins = $this->passwords->findUserByEmail($email); @@ -1331,7 +1341,7 @@ sub getEmails { } } else { - if ( !$Foswiki::cfg{TopicUserMapping}{ForceManageEmails} + if ( !$this->app->cfg->data->{TopicUserMapping}{ForceManageEmails} && $this->passwords->isManagingEmails() ) { @@ -1371,7 +1381,7 @@ sub setEmails { my $this = shift; my $user = shift; - if ( !$Foswiki::cfg{TopicUserMapping}{ForceManageEmails} + if ( !$this->app->cfg->data->{TopicUserMapping}{ForceManageEmails} && $this->passwords->isManagingEmails() ) { $this->passwords->setEmails( $this->getLoginName($user), @_ ); @@ -1400,7 +1410,7 @@ sub mapper_getEmails { my $topicObject = Foswiki::Meta->load( $app, - $Foswiki::cfg{UsersWebName}, + $app->cfg->data->{UsersWebName}, $app->users->getWikiName($user) ); @@ -1445,7 +1455,7 @@ sub mapper_setEmails { my $user = $app->users->getWikiName($cUID); my $topicObject = - Foswiki::Meta->load( $app, $Foswiki::cfg{UsersWebName}, $user ); + Foswiki::Meta->load( $app, $app->cfg->data->{UsersWebName}, $user ); if ( $topicObject->get('FORM') ) { @@ -1492,7 +1502,7 @@ sub findUserByWikiName { if ( $this->isGroup($wn) ) { push( @users, $wn ); } - elsif ( $Foswiki::cfg{Register}{AllowLoginName} ) { + elsif ( $this->app->cfg->data->{Register}{AllowLoginName} ) { # print STDERR "AllowLoginName discovered \n"; @@ -1548,8 +1558,8 @@ sub checkPassword { # If we don't have a PasswordManager and use TemplateLogin, always allow login return 1 - if ( $Foswiki::cfg{PasswordManager} eq 'none' - && $Foswiki::cfg{LoginManager} eq + if ( $this->app->cfg->data->{PasswordManager} eq 'none' + && $this->app->cfg->data->{LoginManager} eq 'Foswiki::LoginManager::TemplateLogin' ); return $this->passwords->checkPassword( $login, $pw ); @@ -1651,10 +1661,9 @@ sub _cacheUser { # callback for search function to collate results sub _collateGroups { - my $ref = shift; - my $group = shift; + my ( $ref, $group ) = @_; return unless $group; - push( @{ $ref->{list} }, $group ); + push @{ $ref->{list} }, $group; } # get a list of groups defined in this Wiki @@ -1671,7 +1680,7 @@ sub _getListOfGroups { $this->groupsList( [] ); #create a MetaCache _before_ we do silly things with the app's users - $app->search->metacache(); + $app->search->metacache; # Temporarily set the user to admin, otherwise it cannot see groups # where %USERSWEB% is protected from view @@ -1681,13 +1690,14 @@ sub _getListOfGroups { try { $app->user( $app->cfg->data->{SuperAdminGroup} ); + $app->search->searchWeb( _callback => \&_collateGroups, _cbdata => { list => $this->groupsList, users => $users }, - web => $Foswiki::cfg{UsersWebName}, + web => $app->cfg->data->{UsersWebName}, topic => "*Group", scope => 'topic', search => '1', @@ -1698,7 +1708,7 @@ sub _getListOfGroups { nototal => 'on', noempty => 'on', format => '$topic', - separator => '', + separator => '' ); } catch { @@ -1720,25 +1730,27 @@ sub _loadMapping { return if $this->CACHED; $this->CACHED(1); + my $cfgData = $this->app->cfg->data; + #TODO: should only really do this mapping IF the user is in the password file. # except if we can't 'fetchUsers' like in the Passord='none' case - # in which case the only time we # know a login is real, is when they are logged in :( - if ( ( $Foswiki::cfg{Register}{AllowLoginName} ) + if ( ( $cfgData->{Register}{AllowLoginName} ) || ( !$this->passwords->canFetchUsers() ) ) { my $app = $this->app; if ( $app->store->topicExists( - $Foswiki::cfg{UsersWebName}, - $Foswiki::cfg{UsersTopicName} + $cfgData->{UsersWebName}, + $cfgData->{UsersTopicName} ) ) { my $usersTopicObject = Foswiki::Meta->load( $app, - $Foswiki::cfg{UsersWebName}, - $Foswiki::cfg{UsersTopicName} + $cfgData->{UsersWebName}, + $cfgData->{UsersTopicName} ); my $text = $usersTopicObject->text() || ''; @@ -1766,6 +1778,8 @@ s/^\s*\* (?:$Foswiki::regex{webNameRegex}\.)?($Foswiki::regex{wikiWordRegex})\s* sub _expandUserList { my ( $this, $names, $expand ) = @_; + my $cfgData = $this->app->cfg->data; + $expand = 1 unless ( defined $expand ); # print STDERR "_expandUserList called $names - expand $expand \n"; @@ -1780,7 +1794,7 @@ sub _expandUserList { foreach my $ident ( split( /[\,\s]+/, $names ) ) { # Dump the web specifier if userweb - $ident =~ s/^($Foswiki::cfg{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//; + $ident =~ s/^($cfgData->{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//; next unless $ident; if ( $this->isGroup($ident) ) { if ( !$expand ) { diff --git a/UnitTestContrib/lib/Unit/FoswikiTestRole.pm b/UnitTestContrib/lib/Unit/FoswikiTestRole.pm new file mode 100644 index 0000000000..c437e31726 --- /dev/null +++ b/UnitTestContrib/lib/Unit/FoswikiTestRole.pm @@ -0,0 +1,763 @@ +# See bottom of file for license and copyright + +=begin TML + +---+ Role Unit::FoswikiTestRole + +This role provide methods common to all high-level test case classes bound to +Foswiki API. + +=cut + +package Unit::FoswikiTestRole; + +use Assert; +use Try::Tiny; +use File::Spec; +use Scalar::Util qw(blessed); + +BEGIN { + if (Unit::TestRunner::CHECKLEAK) { + eval "use Devel::Leak::Object qw{ GLOBAL_bless };"; + die $@ if $@; + $Devel::Leak::Object::TRACKSOURCELINES = 1; + $Devel::Leak::Object::TRACKSTACK = 1; + } +} + +# Use variable to let it be easily incorporated into a regex. +our $TEST_WEB_PREFIX = 'Temporary'; + +use Moo::Role; + +our @mails; + +has app => ( + is => 'rw', + lazy => 1, + predicate => 1, + clearer => 1, + isa => Foswiki::Object::isaCLASS( 'app', 'Unit::TestApp', noUndef => 1, ), + default => sub { + if ( defined $Foswiki::app ) { + return $Foswiki::app; + } + return Unit::TestApp->new( env => \%ENV ); + }, + handles => [qw(create)], +); +has test_web => ( + is => 'rw', + lazy => 1, + clearer => 1, + default => sub { return $_[0]->testWebName; }, +); +has test_topic => ( + is => 'rw', + lazy => 1, + default => sub { return 'TestTopic' . $_[0]->testSuite; }, +); +has users_web => ( + is => 'rw', + lazy => 1, + builder => 'prepareUsersWeb', +); + +has _holderStack => ( is => 'rw', lazy => 1, default => sub { [] }, ); +has _testWebs => ( + is => 'rw', + lazy => 1, + clearer => 1, + predicate => 1, + default => sub { [] }, +); + +has __FoswikiSafe => ( is => 'rw', ); +has __EnvSafe => ( + is => 'rw', + lazy => 1, + clearer => 1, + default => sub { {} }, +); + +=begin TML + +---++ ObjectAttribute __EnvReset + +__EnvReset defines environment variables to be deleted or set to predefined +values. If a variable key has undefined value then it is deleted. Otherwise it +is set to the value defined. + +=cut + +has __EnvReset => ( + is => 'rw', + lazy => 1, + clearer => 1, + default => sub { {} }, +); + +around set_up => sub { + my $orig = shift; + my $this = shift; + + $orig->( $this, @_ ); + + @mails = (); +}; + +around tear_down => sub { + my $orig = shift; + my $this = shift; + + @mails = (); + $this->app->net->setMailHandler( \&sentMail ); + + $orig->( $this, @_ ); +}; + +sub prepareUsersWeb { + return $TEST_WEB_PREFIX . $_[0]->testSuite . 'UsersWeb'; +} + +=begin TML + +---++ ObjectMethod registerUser($loginname, $forename, $surname, $email) + +Can be used by subclasses to register test users. + +=cut + +sub registerUser { + my ( $this, $loginname, $forename, $surname, $email ) = @_; + + my $cfgData = $this->app->cfg->data; + + $this->saveState; + + my $reqParams = { + 'TopicName' => ['UserRegistration'], + 'Twk1Email' => [$email], + 'Twk1WikiName' => ["$forename$surname"], + 'Twk1Name' => ["$forename $surname"], + 'Twk0Comment' => [''], + 'Twk1FirstName' => [$forename], + 'Twk1LastName' => [$surname], + 'action' => ['register'] + }; + + if ( $cfgData->{Register}{AllowLoginName} ) { + $reqParams->{"Twk1LoginName"} = $loginname; + } + + $this->createNewFoswikiApp( + requestParams => { initializer => $reqParams, }, + engineParams => { + initialAttributes => { + path_info => "/" . $this->users_web . "/UserRegistration", + method => 'POST', + user => $this->app->cfg->data->{AdminUserLogin}, + action => 'register', + }, + }, + callbacks => { + + # Get around the default application exception handling to stay in + # control of failed registration without the need to analyze HTML + # output of handleRequest() method. + handleRequestException => sub { + my $this = shift; + my %args = @_; + $args{params}{exception}->rethrow; + }, + }, + ); + $this->assert( + $this->app->store->topicExists( + $this->test_web, $cfgData->{WebPrefsTopicName} + ) + ); + + $this->app->net->setMailHandler( \&sentMail ); + $this->app->cfg->data->{Validation}{Method} = 'none'; + try { + $this->app->handleRequest; + + #my $uiRegister = $this->create('Foswiki::UI::Register'); + #$uiRegister->register_cgi; + + #$this->captureWithKey( register_cgi => sub { $uiRegister->register_cgi } + #); + } + catch { + my $e = $_; + if ( $e->isa('Foswiki::OopsException') ) { + $this->assert_str_equals( "register", $e->{template}, + $e->stringify() ); + $this->assert_str_equals( "thanks", $e->{def}, $e->stringify() ); + } + elsif ( $e->isa('Foswiki::AccessControlException') ) { + $this->assert( 0, $e->stringify ); + } + elsif ( $e->isa('Foswiki::Exception') ) { + $this->assert( 0, $e->stringify ); + } + else { + $this->assert( 0, "expected an oops redirect" ); + } + }; + + $this->restoreState; + + # Reset + $this->app->users->mapping->invalidate; +} + +=begin TML + +---++ StaticMethod sentMail($net, $mess) + +Default implementation for the callback used by Net.pm. Sent mails are +pushed onto a global variable @FoswikiFnTestCase::mails. + +=cut + +sub sentMail { + my ( $net, $mess ) = @_; + push( @mails, $mess ); + return undef; +} + +=begin TML + +---++ ObjectMethod saveState + +Preserves current state of test object. This method is utilizing +=Foswiki::Aux::Localize= facilities. + +=cut + +sub saveState { + my $this = shift; + my %params; + + my $holderObj = $this->localize(@_); + + push @{ $this->_holderStack }, $holderObj; +} + +=begin TML + +---++ ObjectMethod restoreState + +Restores last saved by =saveState= method object state. In addition to +functionality provided by =Foswiki::Aux::Localize= this method also restore the +application and config globals =$Foswiki::app= and =%Foswiki::cfg=, +correspondingly. + +=cut + +sub restoreState { + my $this = shift; + + ASSERT( @{ $this->_holderStack } > 0, "Empty stack of holder objects" ) + if DEBUG; + + pop @{ $this->_holderStack }; + + $Foswiki::app = $this->app; + + $this->app->cfg->assignGLOB; + $this->_fixupAppObjects; +} + +=begin TML +---++ ObjectMethod preserveEnvironment + +Preserves current run environment including =%ENV= and config. + +=cut + +sub preserveEnvironment { + my $this = shift; + + $this->_clear__EnvSafe; + foreach my $sym ( keys %ENV ) { + next unless defined($sym); + $this->__EnvSafe->{$sym} = $ENV{$sym}; + } + + foreach my $sym ( keys %{ $this->__EnvReset } ) { + if ( defined $this->__EnvReset->{$sym} ) { + $ENV{$sym} = $this->__EnvReset->{$sym}; + } + else { + delete $ENV{$sym}; + } + } + + $this->__FoswikiSafe( + $this->app->cfg->_cloneData( $this->app->cfg->data, 'data' ) ); +} + +=begin TML +---++ ObjectMethod restoreEnvironment + +Restores run environment preserved by =preserveEnvironment= method. + +=cut + +sub restoreEnvironment { + my $this = shift; + $this->app->cfg->data( $this->__FoswikiSafe ); + foreach my $sym ( keys %ENV ) { + unless ( defined( $this->__EnvSafe->{$sym} ) ) { + delete $ENV{$sym}; + } + else { + $ENV{$sym} = $this->__EnvSafe->{$sym}; + } + } +} + +=begin TML + +---++ ObjectMethod setupPlugins + +Disable/enable plugins so that only core extensions (those defined in +lib/MANIFEST) are enabled, but they are *all* enabled. + +=cut + +sub setupPlugins { + my $this = shift; + + my $cfgData = $this->app->cfg->data; + + # First disable all plugins + foreach my $k ( keys %{ $cfgData->{Plugins} } ) { + next unless ref( $cfgData->{Plugins}{$k} ) eq 'HASH'; + $cfgData->{Plugins}{$k}{Enabled} = 0; + } + + # then reenable only those listed in MANIFEST + my $home = $ENV{FOSWIKI_HOME} || '../..'; + $home = '../..' unless -e "$ENV{FOSWIKI_HOME}/lib/MANIFEST"; + open( F, "$home/lib/MANIFEST" ) || die $!; + my @moreConfig; + local $/ = "\n"; + while () { + if (/^!include .*?([^\/]+)\/([^\/]+)$/) { + my ( $subdir, $extension ) = ( $1, $2 ); + chomp $extension; + + # Don't enable EmptyPlugin - Disabled by default + if ( $extension =~ m/Plugin$/ && $extension ne 'EmptyPlugin' ) { + unless ( exists $cfgData->{Plugins}{$extension}{Module} ) { + $cfgData->{Plugins}{$extension}{Module} = + 'Foswiki::Plugins::' . $extension; + print STDERR "WARNING: $extension has no module defined, " + . "it might not load!\n" + . "\tGuessed it to $cfgData->{Plugins}{$extension}{Module}\n"; + } + $cfgData->{Plugins}{$extension}{Enabled} = 1; + } + + # Is there a Config.spec? + if ( + open( G, "<", + "../../lib/Foswiki/$subdir/$extension/Config.spec" + ) + ) + { + local $/ = undef; + my $config = ; + close(G); + + # Add the config unless already defined in LocalSite.cfg + $config =~ +s/((\$Foswiki::cfg\{.*?\})\s*=.*?;)(?:\n|$)/push(@moreConfig, $1) unless (eval "exists $2"); ''/ges; + } + } + } + close(F); + + # Additional config picked up from plugins Config.spec's + if ( scalar @moreConfig ) { + unshift( @moreConfig, 'my $FALSE = 0; my $TRUE = 1;' ); + my $cmd = join( "\n", @moreConfig ); + + #print STDERR $cmd; # Additional config from enabled extensions + eval $cmd; + die $@ if $@; + } + + # Take a look at installed contribs and see if they demand any + # additional setup. + if ( opendir( F, "$home/lib/Foswiki/Contrib" ) ) { + foreach my $d ( grep { /^[A-Za-z]+Contrib$/ } readdir(F) ) { + next unless -e "$home/lib/Foswiki/Contrib/$d/UnitTestSetup.pm"; + my $setup = "Foswiki::Contrib::$d" . '::UnitTestSetup'; + $setup =~ m/^(.*)$/; # untaint + Foswiki::load_package($setup); + $setup->set_up(); + } + closedir(F); + } +} + +=begin TML +---++ ObjectMethod setupDirs + +Takes measures as to avoid polluting the base directory with test data and logs. + +=cut + +sub setupDirs { + my $this = shift; + + my $cfgData = $this->app->cfg->data; + + $cfgData->{WorkingDir} = $this->tempDir; + foreach my $subdir (qw(tmp registration_approvals work_areas requestTmp)) { + my $newDir = + File::Spec->catfile( $this->app->cfg->data->{WorkingDir}, $subdir ); + ASSERT( mkdir($newDir), "mkdir($newDir) : $!" ); + } + + # Note this does not do much, except for some tests that use it directly. + # The first call to File::Temp caches the temp directory name, so + # this value won't get used for anything created by File::Temp + $cfgData->{TempfileDir} = $cfgData->{WorkingDir} . "/requestTmp"; + + # Move logging into a temporary directory + my $logdir = Cwd::getcwd() . '/testlogs'; + $logdir =~ m/^(.*)$/; + $logdir = $1; + $cfgData->{Log}{Dir} = $logdir; + mkdir($logdir) unless -d $logdir; + my $logName = $this->testSuite; + $cfgData->{Log}{Implementation} = 'Foswiki::Logger::Compatibility'; + $cfgData->{LogFileName} = "$logdir/$logName.log"; + $cfgData->{WarningFileName} = "$logdir/$logName.warn"; + $cfgData->{DebugFileName} = "$logdir/$logName.debug"; +} + +=begin TML + +---++ ObjectMethod setupAdminUser(%userData) + +Sets this test administrator user data. The =%userData= hash may have the +following keys: + +|*Key*|*Description*|*Default*| +|=wikiname=|Admin's wiki name|_AdminUser_| +|=login=|Admin's login|_root_| +|=group=|Administrative group|_AdminGroup_| + +=cut + +sub setupAdminUser { + my $this = shift; + my %userData = @_; + my $cfgData = $this->app->cfg->data; + + $cfgData->{AdminUserWikiName} = $userData{wikiname} || 'AdminUser'; + $cfgData->{AdminUserLogin} = $userData{login} || 'root'; + $cfgData->{SuperAdminGroup} = $userData{group} || 'AdminGroup'; +} + +=begin TML +---++ ObjectMethod setupUserRegistration + +Configures components needed to register new users so as to avoid polluting the +base installation. + +=cut + +sub setupUserRegistration { + my $this = shift; + + my $cfgData = $this->app->cfg->data; + + $cfgData->{Register}{AllowLoginName} = 1; + my $htFile = $cfgData->{Htpasswd}{FileName} = + $cfgData->{WorkingDir} . "/htpasswd"; + $cfgData->{Htpasswd}{LockFileName} = + $cfgData->{WorkingDir} . "/htpasswd.lock"; + unless ( -e $htFile ) { + my $fh; + open( $fh, ">:encoding(utf-8)", $htFile ) + || Foswiki::Exception::FileOp->throw( + file => $htFile, + op => 'open', + ); + close($fh) || Foswiki::Exception::FileOp->throw( + file => $htFile, + op => 'close', + ); + } + $cfgData->{PasswordManager} = 'Foswiki::Users::HtPasswdUser'; + $cfgData->{Htpasswd}{GlobalCache} = 0; + $cfgData->{UserMappingManager} = 'Foswiki::Users::TopicUserMapping'; + $cfgData->{LoginManager} = 'Foswiki::LoginManager::TemplateLogin'; + $cfgData->{Register}{EnableNewUserRegistration} = 1; + $cfgData->{RenderLoggedInButUnknownUsers} = 0; + + $cfgData->{Register}{NeedVerification} = 0; + $cfgData->{MinPasswordLength} = 0; + $cfgData->{UsersWebName} = $this->users_web; +} + +=begin TML + +---++ ObjectMethod createNewFoswikiApp(%params) -> ref to new Unit::TestApp obj + +cleans up the existing Foswiki object, and creates a new one + +=%params= are passed directly to the =Foswiki::App= constructor. + +typically called to force a full re-initialisation either with new preferences, topics, users, groups or CFG + +=cut + +# Correct all Foswiki::AppObject to use currently active Foswiki::App object. +# SMELL Hacky but shall be transparent for any derived test case class. +sub _fixupAppObjects { + my $this = shift; + + my $app = $this->app; + + foreach my $attr ( keys %$this ) { + if ( + blessed( $this->{$attr} ) + && $this->$attr->isa('Foswiki::Object') + && $this->$attr->can('_set_app') + && ( !defined( $this->$attr->app ) + || ( $this->$attr->app != $app ) ) + ) + { + $this->$attr->_set_app($app); + } + } +} + +sub createNewFoswikiApp { + my $this = shift; + + my $app = $this->app; + my %params = @_; + + $app->cfg->data->{Store}{Implementation} ||= 'Foswiki::Store::PlainFile'; + + $params{env} //= $app->cloneEnv; + my $newApp = Unit::TestApp->new( cfg => $app->cfg->clone, %params ); + + $this->app($newApp); + $this->_fixupAppObjects; + + # WorkDir is set to _tempDir but _tempDir might be cleaned up before $app + # gets completely shutdown. This draws some app frameworks to fail upon + # cleanup as they rely upon WorkDir. By storing the _tempDir object on app's + # heap we let them shutdown cleanly. + $newApp->heap->{TestCase_TempDir} = $this->_tempDir; + + return $newApp; +} + +=begin TML +---++ ObjectMethod testWebName($baseName) -> $webName + +Returns a standard test web name formed with test suite name and =$baseName=. +If =$baseName= is undef then it is set to the test suite name. + +=cut + +sub testWebName { + my $this = shift; + my ($baseName) = @_; + + $baseName //= $this->testSuite; + + return $TEST_WEB_PREFIX . $this->testSuite . 'TestWeb' . $baseName; +} + +=begin TML + +---++ ObjectMethod populateStandardWebs + +Creates standard test webs defined by =test_web= and =users_web= attributes. + +=cut + +sub populateStandardWebs { + my $this = shift; + + foreach my $web ( $this->test_web, $this->users_web ) { + $this->populateNewWeb($web); + } +} + +=begin TML + +---++ ObjectMethod cleanupTestWebs + +Deletes all test webs recorded by =Unit::FoswikiTestRole= =populateNewWeb()= +method. + +=cut + +sub cleanupTestWebs { + my $this = shift; + + if ( $this->_has_testWebs ) { + foreach my $web ( @{ $this->_testWebs } ) { + $this->removeTestWeb($web); + } + $this->_clear_testWebs; + } +} + +=begin TML + +---++ ObjectMethod populateNewWeb($web, $template, $opts) => $webObject + +Creates a new web. If the web already exists then deletes it first. Parameters +are the same as in =Foswiki::Meta= =populateNewWeb()= method. + +The created web is then recorded internally. All recorded webs can be removed +using =cleanupTestWebs()= method. + +Returns newly created =Foswiki::Meta= web object. + +=cut + +sub populateNewWeb { + my $this = shift; + my ( $web, $template, $opts ) = @_; + + if ( $this->app->store->webExists($web) ) { + $this->removeTestWeb($web); + } + my $webObj = $this->create( 'Foswiki::Meta', web => $web ); + ASSERT( defined $webObj, "Failed to create new web `$web'" ); + $webObj->populateNewWeb( $template, $opts ); + push @{ $this->_testWebs }, $web; + + return $webObj; +} + +=begin TML + +---++ ObjectMethod removeTestWeb($web) + +Remove a temporary web fixture (data and pub). + +The web name will be checked for 'Temporary' prefix and exception will be thrown +if the prefix is not in place. This is to protect non-test webs from being +accidentally removed. If a test web doesn't have the prefix then it must be +removed manually. To generate names corresponding to the requirement use +=testWebName()= method. + +=cut + +sub removeTestWeb { + my ( $this, $web ) = @_; + + unless ( $web =~ /^$TEST_WEB_PREFIX/ ) { + Foswiki::Exception::Fatal->throw( text => "Cannot remove test web " + . $web + . " because it's name doesn't start with " + . $TEST_WEB_PREFIX ); + } + + try { + my $webObject = $this->create( 'Foswiki::Meta', web => $web ); + $webObject->removeFromStore(); + } + catch { + say STDERR "Unexpected exception while removing web $web"; + say STDERR Foswiki::Exception::errorStr($_); + }; +} + +=begin TML + +---++ ObjectMethod leakDetectCheckpoint + +This method sets a checkpoint for =Devel::Leak= if =Unit::TestRunner::CHECKLEAK= +is true. + +Use it with big care as it may mask out all leakages happened before this method +was called. + +=cut + +sub leakDetectCheckpoint { + my $this = shift; + my ($dumpName) = @_; + + return unless Unit::TestRunner::CHECKLEAK; + + $dumpName //= $this->testSuite; + + say STDERR "<<< LEAK CHECKPOINT FOR TEST ", $dumpName; + + return Devel::Leak::Object::checkpoint(); +} + +=begin TML + +---++ ObjectMethod leakDetectDump( $dumpName ) + +Dumps current state of of leaked objects in memory using +=Devel::Leak::Object::status()= call. =$dumpName= is used to distinguish a +particular dump from other. + +If module =Devel::MAT::Dumper= is present +then memory dump would be made into log dir into a _.pmat_ file named after +=$dumpName=. + +Do nothing unless =Unit::TestRunner::CHECKLEAK= is true. + +=cut + +sub leakDetectDump { + my $this = shift; + my ($dumpName) = @_; + + return unless Unit::TestRunner::CHECKLEAK; + + $dumpName //= $this->testSuite; + + $dumpName =~ tr/:/_/; + say STDERR ">>> LEAK DUMP FOR TEST ", $dumpName; + Devel::Leak::Object::status(); + eval { + require Devel::MAT::Dumper; + my $pmatFile = File::Spec->catfile( $this->app->cfg->data->{Log}{Dir}, + $dumpName . ".pmat" ); + say STDERR "Dumping Devel::MAT data into $pmatFile"; + Devel::MAT::Dumper::dump($pmatFile); + }; +} + +1; +__END__ +Foswiki - The Free and Open Source Wiki, http://foswiki.org/ + +Copyright (C) 2016 Foswiki Contributors. Foswiki Contributors +are listed in the AUTHORS file in the root of this distribution. +NOTE: Please extend that file, not this notice. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. For +more details read LICENSE in the root of this distribution. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +As per the GPL, removal of this notice is prohibited. diff --git a/UnitTestContrib/lib/Unit/PlackTestCase.pm b/UnitTestContrib/lib/Unit/PlackTestCase.pm index e05fc42cf8..77c382bb45 100644 --- a/UnitTestContrib/lib/Unit/PlackTestCase.pm +++ b/UnitTestContrib/lib/Unit/PlackTestCase.pm @@ -1,32 +1,26 @@ # See bottom of file for license and copyright information +=begin TML + +---+ package Unit::PlackTestCase + +Base class for all =Plack::Test= based tests. + +=cut + package Unit::PlackTestCase; use v5.14; use Plack::Test; -use FindBin; +use File::Spec; +use Assert; +use Try::Tiny; +use HTML::Parser; +require Unit::TestRunner; -use Moo; -use namespace::clean; +use Foswiki::Class; extends qw(Unit::TestCase); - -BEGIN { - if (Unit::TestRunner::CHECKLEAK) { - eval "use Devel::Leak::Object qw{ GLOBAL_bless };"; - die $@ if $@; - $Devel::Leak::Object::TRACKSOURCELINES = 1; - $Devel::Leak::Object::TRACKSTACK = 1; - } -} - -has app => ( - is => 'rw', - weak_ref => 1, - lazy => 1, - predicate => 1, - clearer => 1, - isa => Foswiki::Object::isaCLASS( 'app', 'Unit::TestApp', noUndef => 1, ), -); +with qw(Foswiki::Aux::Localize Unit::FoswikiTestRole); =begin TML @@ -36,8 +30,14 @@ List of hashrefs with test parameters. Keys: - * =app= + * =app= + * =appClass= + * =appParams= * =client= - required, client sub + * =init= – additional init sub for test + * =adminUser= - hashref, {wikiname=>'AdminUserWikiName', login => 'admin', group => 'AdminGroup',}; see =Unit::FoswikiTestRole= =setupAdminUser= method. + * =testWebs= – hash of test webs to be created for the test where each key is a hashref to topicName => "topic text", + * =testUsers= - array of users to be registered for testing purposes. Each element is a hashref with keys =login=, =forename=, =surname=, =email=, =group=; see =Unit::FosdwikiTestRole= =registerUser()= method. =cut @@ -52,20 +52,127 @@ has defaultAppClass => ( default => 'Unit::TestApp', ); -sub initialize { +around set_up => sub { + my $orig = shift; + my $this = shift; + + $this->leakDetectCheckpoint( $this->testSuite ); + + return $orig->( $this, @_ ); +}; + +around tear_down => sub { + my $orig = shift; + my $this = shift; + + $this->leakDetectDump( $this->testSuite ); + + return $orig->( $this, @_ ); +}; + +# Executes coderefs defined by test parameters keys initTest, initRequest, shutdownTest, shutdownRequest. +sub _execPerTestStageCode { + my $this = shift; + my $stageName = shift; + my %args = @_; + + if ( defined( $args{testParams}{$stageName} ) ) { + my $stageSub = $args{testParams}{$stageName}; + $this->assert( ref($stageSub) eq 'CODE', + "testParams $stageName key must be a coderef" ); + $stageSub->( $this, @_ ); + } +} + +sub initTest { my $this = shift; my %args = @_; - if ( defined $args{testParams}{init} ) { - my $init = $args{testParams}{init}; - $this->assert( ref($init) eq 'CODE', - "testParams init key must be a coderef" ); - $init->( $this, %args ); + my $params = $args{testParams}; + + $this->preserveEnvironment; + + $this->setupPlugins; + $this->setupDirs; + $this->setupUserRegistration; + $this->setupAdminUser( %{ $params->{adminUser} // {} } ); + $this->populateStandardWebs; + + if ( defined $params->{testWebs} ) { + $this->assert( + ref( $params->{testWebs} ) eq 'HASH', + "Test parameters testWebs key isn't a hashref" + ); + + foreach my $web ( keys %{ $params->{testWebs} } ) { + $this->populateNewWeb($web); + if ( defined $params->{testWebs}{$web} ) { + my $topics = $params->{testWebs}{$web}; + ASSERT( ref($topics) eq 'HASH', + "Test web $web topics must be defined with a hashref" ); + foreach my $topic ( keys %{$topics} ) { + $this->writeTopic( $web, $topic, $topics->{$topic} ); + } + } + } + } + + if ( defined $params->{testUsers} ) { + $this->assert( + ref( $params->{testUsers} ) eq 'ARRAY', + "Test parameters testUsers key isn't an arrayref" + ); + + foreach my $user ( @{ $params->{testUsers} } ) { + $this->assert( + defined($user) && ( ref($user) eq 'HASH' ), + "Non-hashref element of testUsers array" + ); + my @registerKeys = qw(login forename surname email); + $this->assert( defined $user->{$_}, + "Undefined key `$_' in user element of testUsers array" ) + foreach @registerKeys; + $this->registerUser( @{$user}{@registerKeys} ); + if ( $user->{group} ) { + unless ( $this->app->users->isGroup( $user->{group} ) ) { + $this->assert( + $this->app->addUserToGroup( + $this->app->user, $user->{group}, 1 + ) + ); + } + $this->assert( + $this->app->addUserToGroup( + $user->{login}, $user->{group} + ) + ); + } + } } + + $this->_execPerTestStageCode( 'initTest', @_ ); } -sub shutdown { +sub shutdownTest { my $this = shift; + + $this->_execPerTestStageCode( 'shutdownTest', @_ ); + + $this->cleanupTestWebs; + $this->_clear_tempDir; + $this->restoreEnvironment; +} + +sub initRequest { + my $this = shift; + + $this->_execPerTestStageCode( 'initRequest', @_ ); +} + +sub shutdownRequest { + my $this = shift; + + $this->_execPerTestStageCode( 'shutdownRequest', @_ ); } around list_tests => sub { @@ -80,15 +187,24 @@ around list_tests => sub { $this->assert_not_null( $clientHash->{name}, "client test name undefined" ); - unless ( defined $clientHash->{app} ) { - $clientHash->{app} = $this->_genDefaultAppSub($clientHash); + unless ( defined $clientHash->{appSub} ) { + $clientHash->{appSub} = $this->_genDefaultAppSub($clientHash); } - my $testSubName = "test_$clientHash->{name}"; + my $testSubName = "test_" . $clientHash->{name}; unless ( $suite->can($testSubName) ) { no strict 'refs'; *{"$suite\:\:$testSubName"} = sub { - my $test = Plack::Test->create( $clientHash->{app} ); - $clientHash->{client}->( $this, $test ); + my $test = Plack::Test->create( $clientHash->{appSub} ); + $this->initTest( + testParams => $clientHash, + testObject => $test + ); + $clientHash->{client} + ->( $this, testParams => $clientHash, testObject => $test, ); + $this->shutdownTest( + testParams => $clientHash, + testObject => $test + ); }; use strict 'refs'; } @@ -113,14 +229,15 @@ sub prepareTestClientList { return \@tests; } -sub _cbPreHandleRequest { +sub _cbPostConfig { my $this = shift; my $app = shift; my $clientHash = shift; my %args = @_; + $this->saveState; $this->app($app); - $this->initialize( %args, testParams => $clientHash, ); + $this->initRequest( %args, testParams => $clientHash ); } sub _cbPostHandleRequest { @@ -129,25 +246,22 @@ sub _cbPostHandleRequest { my $clientHash = shift; my %args = @_; - $this->shutdown( %args, testParams => $clientHash, ); + $this->shutdownRequest( %args, testParams => $clientHash, ); + $this->restoreState; } sub _genDefaultAppSub { my $this = shift; my ($clientHash) = @_; - my %runArgs; - foreach my $key ( grep { !/^(?:app|appClass|client)$/ } keys %$clientHash ) - { - $runArgs{$key} = $clientHash->{$key}; - } + my %runArgs = %{ $clientHash->{appParams} // {} }; my $appClass = $clientHash->{appClass} // $this->defaultAppClass; # Users must not use this callback. - $runArgs{callbacks}{testPreHandleRequest} = sub { + $runArgs{callbacks}{postConfig} = sub { my $app = shift; - $this->_cbPreHandleRequest( $app, $clientHash, @_ ); + $this->_cbPostConfig( $app, $clientHash, @_ ); }; $runArgs{callbacks}{testPostHandleRequest} = sub { my $app = shift; @@ -157,27 +271,136 @@ sub _genDefaultAppSub { return sub { my $env = shift; - Devel::Leak::Object::checkpoint() if Unit::TestRunner::CHECKLEAK; - - $runArgs{env} //= $env; + $runArgs{env} = { ( %$env, %{ $clientHash->{appParams}{env} // {} } ) }; my $rc = $appClass->run(%runArgs); - if (Unit::TestRunner::CHECKLEAK) { - Devel::Leak::Object::status(); - eval { - require Devel::MAT::Dumper; - Devel::MAT::Dumper::dump( $FindBin::Bin - . "/../working/logs/" - . $this->testSuite - . ".pmat" ); - }; - } - return $rc; }; } +sub writeTopic { + my $this = shift; + my ( $web, $topic, $text ) = @_; + + my ($topicObj) = $this->app->readTopic( $web, $topic ); + ASSERT( defined $topicObj, "Failed to read or create topic `$topic'" ); + $topicObj->text($text); + $topicObj->save; + return $topicObj; +} + +=begin TML + +$this->findHTMLTag( + tag => 'a', + class => qr//, + text => 'Attach', +); + +=cut + +sub _smartMatch { + my $this = shift; + my ( $text, $pattern ) = @_; + + if ( my $refType = ref($pattern) ) { + if ( $refType eq 'Regexp' ) { + return $text =~ $pattern; + } + elsif ( $refType eq 'CODE' ) { + return $pattern->( $this, $text ); + } + ASSERT( 0, "Cannot match against $refType reference" ); + } + return $text eq $pattern; +} + +sub findHTMLTag { + my $this = shift; + my $html = shift; + my %params = @_; + + ASSERT( defined $params{tag}, "tag key required" ); + my $tagPat = $params{tag}; + my $textPat = $params{text}; + + $tagPat = lc($tagPat) unless ref($tagPat); + + delete @params{qw(tag text)}; + + my $parser = HTML::Parser->new( api_version => 3, ); + + my @entStack; + my $lastCandidate; + + my $matchedEntity; + + $parser->handler( + start => sub { + my ( $tag, $attrs, $p ) = @_; + return unless $this->_smartMatch( $tag, $tagPat ); + my $matches = 1; + foreach my $attrName ( keys %params ) { + $matches &&= defined( $attrs->{$attrName} ) + && $this->_smartMatch( $attrs->{$attrName}, + $params{$attrName} ); + } + my $entity = { + tag => $tag, + attrs => $attrs, + matches => $matches, + }; + if ( $matches && !$textPat ) { + + # No text pattern defined, first matched entity is ok. + $matchedEntity = $entity; + $p->eof; + return; + } + $lastCandidate = $entity if $matches; + push @entStack, $entity; + }, + "tagname,attr,self" + ); + + $parser->handler( + end => sub { + my ( $tag, $text, $p ) = @_; + if ( $lastCandidate + && ( !$textPat || $this->_smartMatch( $text, $textPat ) ) ) + { + $matchedEntity = $lastCandidate; + $p->eof; + return; + } + return unless $this->_smartMatch( $tag, $tagPat ); + my $lastEntity = pop @entStack; + if ( $lastCandidate && $lastEntity eq $lastCandidate ) { + undef $lastCandidate; + } + }, + "tagname,skipped_text,self" + ); + + $parser->parse($html); + + return $matchedEntity; +} + +# Localization support +around setLocalizeFlags => sub { + my $orig = shift; + + # Don't clean app on localizing as we might need it until the new one is + # created. + return $orig->(@_), clearAttributes => 0; +}; + +sub setLocalizableAttributes { + return qw(app); +} + 1; __END__ Foswiki - The Free and Open Source Wiki, http://foswiki.org/ diff --git a/UnitTestContrib/lib/Unit/TestApp.pm b/UnitTestContrib/lib/Unit/TestApp.pm index 3d3594ee0d..34e45e5fd9 100644 --- a/UnitTestContrib/lib/Unit/TestApp.pm +++ b/UnitTestContrib/lib/Unit/TestApp.pm @@ -9,7 +9,6 @@ use Scalar::Util qw(blessed weaken refaddr); use Try::Tiny; use Foswiki::Class qw(callbacks); -use namespace::clean; extends qw(Foswiki::App); callback_names qw(testPreHandleRequest testPostHandleRequest); @@ -114,6 +113,12 @@ sub registerCallbacks { $this->_cbRegistered(1); } +before _prepareConfig => sub { + my $this = shift; + + $this->registerCallbacks; +}; + around _prepareRequest => sub { my $orig = shift; my $this = shift; @@ -132,8 +137,6 @@ around handleRequest => sub { my $orig = shift; my $this = shift; - $this->registerCallbacks; - my $rc; try { $this->callback('testPreHandleRequest'); diff --git a/UnitTestContrib/lib/Unit/TestRunner.pm b/UnitTestContrib/lib/Unit/TestRunner.pm index 609c798957..8a883582df 100644 --- a/UnitTestContrib/lib/Unit/TestRunner.pm +++ b/UnitTestContrib/lib/Unit/TestRunner.pm @@ -18,13 +18,11 @@ use Unit::Eavesdrop (); use Devel::Symdump (); use File::Spec (); -use Moo; -use namespace::clean; - -extends 'Foswiki::Object'; - use Assert; +use Foswiki::Class; +extends qw(Foswiki::Object); + sub CHECKLEAK { 0 || $ENV{FOSWIKI_CHECKLEAK} } BEGIN { diff --git a/UnitTestContrib/test/unit/ConfigureTestCase.pm b/UnitTestContrib/test/unit/ConfigureTestCase.pm index e28ceaf8f0..f3900e9a5a 100644 --- a/UnitTestContrib/test/unit/ConfigureTestCase.pm +++ b/UnitTestContrib/test/unit/ConfigureTestCase.pm @@ -78,8 +78,6 @@ LSC die "Can't open " . $this->lscpath . " for write: $!" if -e $this->lscpath; } - - $| = 1; }; around tear_down => sub { diff --git a/UnitTestContrib/test/unit/Fn_SEARCH.pm b/UnitTestContrib/test/unit/Fn_SEARCH.pm index 0e064b24db..d6e6191887 100644 --- a/UnitTestContrib/test/unit/Fn_SEARCH.pm +++ b/UnitTestContrib/test/unit/Fn_SEARCH.pm @@ -43,7 +43,7 @@ around BUILDARGS => sub { # This test is run in a separate process to be able to reclaim that memory # after the test is complete. sub run_in_new_process { - return 1; + return 0; } our $AElig; diff --git a/UnitTestContrib/test/unit/FormattingTests.pm b/UnitTestContrib/test/unit/FormattingTests.pm index 3c49c816e1..c85ab0059c 100644 --- a/UnitTestContrib/test/unit/FormattingTests.pm +++ b/UnitTestContrib/test/unit/FormattingTests.pm @@ -270,8 +270,6 @@ around tear_down => sub { $this->removeWebFixture('This(is)') if ( Foswiki::Func::webExists('This(is)') ); $orig->( $this, @_ ); - - $| = 0; }; around loadExtraConfig => sub { diff --git a/UnitTestContrib/test/unit/FoswikiFnTestCase.pm b/UnitTestContrib/test/unit/FoswikiFnTestCase.pm index c6363b46e6..28cce9e07e 100644 --- a/UnitTestContrib/test/unit/FoswikiFnTestCase.pm +++ b/UnitTestContrib/test/unit/FoswikiFnTestCase.pm @@ -31,31 +31,10 @@ use Foswiki::UI::Register(); use Try::Tiny; use Carp qw(cluck); -our @mails; - use Moo; use namespace::clean; extends qw(FoswikiTestCase); -has test_web => ( - is => 'rw', - lazy => 1, - clearer => 1, - builder => sub { - my $testSuite = $_[0]->testSuite; - return 'Temporary' . $testSuite . 'TestWeb' . $testSuite; - }, -); -has test_topic => ( - is => 'rw', - lazy => 1, - builder => sub { return 'TestTopic' . $_[0]->testSuite; }, -); -has users_web => ( - is => 'rw', - lazy => 1, - builder => sub { return 'Temporary' . $_[0]->testSuite . 'UsersWeb'; }, -); has test_user_forename => ( is => 'rw', ); has test_user_surname => ( is => 'rw', ); has test_user_wikiname => ( is => 'rw', ); @@ -91,24 +70,7 @@ around loadExtraConfig => sub { $cfgData->{Store}{Implementation} = "Foswiki::Store::PlainFile"; $cfgData->{RCS}{AutoAttachPubFiles} = 0; - $cfgData->{Register}{AllowLoginName} = 1; - $cfgData->{Htpasswd}{FileName} = $cfgData->{WorkingDir} . "/htpasswd"; - unless ( -e $cfgData->{Htpasswd}{FileName} ) { - my $fh; - open( $fh, ">:encoding(utf-8)", $cfgData->{Htpasswd}{FileName} ) - || die $!; - close($fh) || die $!; - } - $cfgData->{PasswordManager} = 'Foswiki::Users::HtPasswdUser'; - $cfgData->{Htpasswd}{GlobalCache} = 0; - $cfgData->{UserMappingManager} = 'Foswiki::Users::TopicUserMapping'; - $cfgData->{LoginManager} = 'Foswiki::LoginManager::TemplateLogin'; - $cfgData->{Register}{EnableNewUserRegistration} = 1; - $cfgData->{RenderLoggedInButUnknownUsers} = 0; - - $cfgData->{Register}{NeedVerification} = 0; - $cfgData->{MinPasswordLength} = 0; - $cfgData->{UsersWebName} = $this->users_web; + $this->setupUserRegistration; }; around set_up => sub { @@ -125,8 +87,6 @@ around set_up => sub { }, ); - @mails = (); - $this->app->net->setMailHandler( \&FoswikiFnTestCase::sentMail ); my $webObject = $this->populateNewWeb( $this->test_web ); undef $webObject; $this->clear_test_topicObject; @@ -165,8 +125,6 @@ around tear_down => sub { $this->removeWebFixture( $cfg->data->{UsersWebName} ); unlink( $Foswiki::cfg{Htpasswd}{FileName} ); - @mails = (); - $orig->( $this, @_ ); }; @@ -183,104 +141,4 @@ sub removeWeb { $this->removeWebFixture($web); } -=begin TML - ----++ StaticMethod sentMail($net, $mess) - -Default implementation for the callback used by Net.pm. Sent mails are -pushed onto a global variable @FoswikiFnTestCase::mails. - -=cut - -sub sentMail { - my ( $net, $mess ) = @_; - push( @mails, $mess ); - return undef; -} - -=begin TML - ----++ ObjectMethod registerUser($loginname, $forename, $surname, $email) - -Can be used by subclasses to register test users. - -=cut - -sub registerUser { - my ( $this, $loginname, $forename, $surname, $email ) = @_; - - $this->pushApp; - - my $reqParams = { - 'TopicName' => ['UserRegistration'], - 'Twk1Email' => [$email], - 'Twk1WikiName' => ["$forename$surname"], - 'Twk1Name' => ["$forename $surname"], - 'Twk0Comment' => [''], - 'Twk1FirstName' => [$forename], - 'Twk1LastName' => [$surname], - 'action' => ['register'] - }; - - if ( $Foswiki::cfg{Register}{AllowLoginName} ) { - $reqParams->{"Twk1LoginName"} = $loginname; - } - - $this->createNewFoswikiApp( - requestParams => { initializer => $reqParams, }, - engineParams => { - initialAttributes => - { path_info => "/" . $this->users_web . "/UserRegistration", }, - }, - ); - $this->assert( - $this->app->store->topicExists( - $this->test_web, $Foswiki::cfg{WebPrefsTopicName} - ) - ); - - $this->app->net->setMailHandler( \&FoswikiFnTestCase::sentMail ); - try { - my $uiRegister = $this->create('Foswiki::UI::Register'); - $this->captureWithKey( - register_cgi => \&Foswiki::UI::Register::register_cgi, - $uiRegister, - ); - } - catch { - my $e = $_; - if ( $e->isa('Foswiki::OopsException') ) { - if ( $this->check_dependency('Foswiki,<,1.2') ) { - $this->assert_str_equals( "attention", $e->{template}, - $e->stringify() ); - $this->assert_str_equals( "thanks", $e->{def}, - $e->stringify() ); - } - else { - $this->assert_str_equals( "register", $e->{template}, - $e->stringify() ); - $this->assert_str_equals( "thanks", $e->{def}, - $e->stringify() ); - } - } - elsif ( $e->isa('Foswiki::AccessControlException') ) { - $this->assert( 0, $e->stringify ); - } - elsif ( $e->isa('Foswiki::Exception') ) { - $this->assert( 0, $e->stringify ); - } - else { - $this->assert( 0, "expected an oops redirect" ); - } - }; - - # Reload caches - #$this->createNewFoswikiApp( requestParams => $q ); - #$this->app->net->setMailHandler( \&FoswikiFnTestCase::sentMail ); - $this->popApp; - - # Reset - $this->app->users->mapping->invalidate; -} - 1; diff --git a/UnitTestContrib/test/unit/FoswikiTestCase.pm b/UnitTestContrib/test/unit/FoswikiTestCase.pm index f38aa3b699..718dc1ed8f 100644 --- a/UnitTestContrib/test/unit/FoswikiTestCase.pm +++ b/UnitTestContrib/test/unit/FoswikiTestCase.pm @@ -47,21 +47,7 @@ our $didOnlyOnceChecks = 0; use Moo; use namespace::clean; extends qw(Unit::TestCase); -with qw(Foswiki::Aux::Localize); - -has app => ( - is => 'rw', - lazy => 1, - predicate => 1, - clearer => 1, - isa => Foswiki::Object::isaCLASS( 'app', 'Unit::TestApp', noUndef => 1, ), - default => sub { - if ( defined $Foswiki::app ) { - return $Foswiki::app; - } - return Unit::TestApp->new( env => \%ENV ); - }, -); +with qw(Foswiki::Aux::Localize Unit::FoswikiTestRole); #has twiki => # ( is => 'rw', clearer => 1, lazy => 1, default => sub { $_[0]->app }, ); @@ -94,35 +80,6 @@ has request => ( isa => Foswiki::Object::isaCLASS( 'request', 'Foswiki::Request', noUndef => 1, ), ); -has test_web => ( is => 'rw', ); -has test_topic => ( is => 'rw', ); - -has _holderStack => ( is => 'rw', lazy => 1, default => sub { [] }, ); - -has __EnvSafe => ( - is => 'rw', - lazy => 1, - clearer => 1, - default => sub { {} }, -); - -=begin TML - ----++ ObjectAttribute __EnvReset - -__EnvReset defines environment variables to be deleted or set to predefined -values. If a variable key has undefined value then it is deleted. Otherwise it -is set to the value defined. - -=cut - -has __EnvReset => ( - is => 'rw', - lazy => 1, - clearer => 1, - default => sub { {} }, -); -has __FoswikiSafe => ( is => 'rw', ); BEGIN { @@ -134,19 +91,6 @@ BEGIN { #$SIG{__DIE__} = sub { Carp::confess $_[0] }; } -# Simulate Foswiki::AppObject -sub create { - my $this = shift; - ASSERT( defined $this->app, - "app attribute is not defined on " . ref($this) . " object" ); - return $this->app->create(@_); -} - -sub _test_with_deps { - my ( $this, $test, %skip_data ) = @_; - -} - =begin TML ---+++ ObjectMethod skip_test_if($test, @skip_data) -> $reason @@ -231,11 +175,14 @@ sub skip_test_if { # Checks we only need to run once per test run sub _onceOnlyChecks { + my $this = shift; return if $didOnlyOnceChecks; + my $cfgData = $this->app->cfg->data; + # Make sure we can create directories in $Foswiki::cfg{DataDir}, otherwise # the tests will mysteriously fail. - my $t = "$Foswiki::cfg{DataDir}/UnitTestCheckDir"; + my $t = $cfgData->{DataDir} . "/UnitTestCheckDir"; if ( -e $t ) { rmdir($t) || die "Could not remove old $t: $!"; } @@ -247,14 +194,15 @@ sub _onceOnlyChecks { # Make sure we can disallow write permissions. Foswiki tests should # always be run as a non-admin user, so that they can test scenarios # where access permissions are denied. - $t = "$Foswiki::cfg{DataDir}/UnitTestCheckFile"; + $t = $cfgData->{DataDir} . "/UnitTestCheckFile"; if ( -e $t ) { unlink($t) || die "Could not remove old $t: $!"; } open( F, '>', $t ) || die "Could not create $t: $!\nUser running tests " - . "has to be able to create files in $Foswiki::cfg{DataDir}"; + . "has to be able to create files in " + . $cfgData->{DataDir}; print F "Blah"; close(F); chmod( 0444, $t ) @@ -682,139 +630,25 @@ around set_up => sub { $orig->( $this, @_ ); my $cfgData = $this->app->cfg->data; - $this->_clear__EnvSafe; - foreach my $sym ( keys %ENV ) { - next unless defined($sym); - $this->__EnvSafe->{$sym} = $ENV{$sym}; - } - # Tell the world we are running unit tests. Nasty, but needed to # avoid corruption of data spaces when unit tests are run alongside # a running wiki. $Foswiki::inUnitTestMode = 1; - # This needs to be a deep copy - $this->__FoswikiSafe( - Data::Dumper->Dump( [ \%Foswiki::cfg ], ['*Foswiki::cfg'] ) ); - - # Disable/enable plugins so that only core extensions (those defined - # in lib/MANIFEST) are enabled, but they are *all* enabled. - - # First disable all plugins - foreach my $k ( keys %{ $cfgData->{Plugins} } ) { - next unless ref( $cfgData->{Plugins}{$k} ) eq 'HASH'; - $cfgData->{Plugins}{$k}{Enabled} = 0; - } - - # then reenable only those listed in MANIFEST - my $home = $ENV{FOSWIKI_HOME} || '../..'; - $home = '../..' unless -e "$ENV{FOSWIKI_HOME}/lib/MANIFEST"; - open( F, "$home/lib/MANIFEST" ) || die $!; - my @moreConfig; - local $/ = "\n"; - while () { - if (/^!include .*?([^\/]+)\/([^\/]+)$/) { - my ( $subdir, $extension ) = ( $1, $2 ); - chomp $extension; - - # Don't enable EmptyPlugin - Disabled by default - if ( $extension =~ m/Plugin$/ && $extension ne 'EmptyPlugin' ) { - unless ( exists $cfgData->{Plugins}{$extension}{Module} ) { - $cfgData->{Plugins}{$extension}{Module} = - 'Foswiki::Plugins::' . $extension; - print STDERR "WARNING: $extension has no module defined, " - . "it might not load!\n" - . "\tGuessed it to $cfgData->{Plugins}{$extension}{Module}\n"; - } - $cfgData->{Plugins}{$extension}{Enabled} = 1; - } - - # Is there a Config.spec? - if ( - open( G, "<", - "../../lib/Foswiki/$subdir/$extension/Config.spec" - ) - ) - { - local $/ = undef; - my $config = ; - close(G); - - # Add the config unless already defined in LocalSite.cfg - $config =~ -s/((\$Foswiki::cfg\{.*?\})\s*=.*?;)(?:\n|$)/push(@moreConfig, $1) unless (eval "exists $2"); ''/ges; - } - } - } - close(F); + # This is about both config and %ENV + $this->preserveEnvironment; - # Additional config picked up from plugins Config.spec's - if ( scalar @moreConfig ) { - unshift( @moreConfig, 'my $FALSE = 0; my $TRUE = 1;' ); - my $cmd = join( "\n", @moreConfig ); - - #print STDERR $cmd; # Additional config from enabled extensions - eval $cmd; - die $@ if $@; - } - - # Take a look at installed contribs and see if they demand any - # additional setup. - if ( opendir( F, "$home/lib/Foswiki/Contrib" ) ) { - foreach my $d ( grep { /^[A-Za-z]+Contrib$/ } readdir(F) ) { - next unless -e "$home/lib/Foswiki/Contrib/$d/UnitTestSetup.pm"; - my $setup = "Foswiki::Contrib::$d" . '::UnitTestSetup'; - $setup =~ m/^(.*)$/; # untaint - eval "require $1" || die $@; - $setup->set_up(); - } - closedir(F); - } + $this->setupPlugins; ASSERT( !defined $Foswiki::app ) if SINGLE_SINGLETONS; - $this->app->cfg->data->{WorkingDir} = $this->tempDir; - foreach my $subdir (qw(tmp registration_approvals work_areas requestTmp)) { - my $newDir = - File::Spec->catfile( $this->app->cfg->data->{WorkingDir}, $subdir ); - ASSERT( mkdir($newDir), "mkdir($newDir) : $!" ); - } - # Force completion of %Foswiki::cfg # This must be done before moving the logging. $cfgData->{Store}{Implementation} = 'Foswiki::Store::PlainFile'; - #$this->pushApp; - #my $tmp = Unit::TestApp->new( - # user => undef, - # env => $this->app->cloneEnv, - # cfg => $this->app->cfg->clone, - #); - #ASSERT( $tmp->cfg->app == $tmp, - # "Object app attr doesn't point to the new app" ); - #ASSERT( defined $Foswiki::app ) if SINGLE_SINGLETONS; - #undef $tmp; # finish() will be called automatically. - #ASSERT( !defined $Foswiki::app ) if SINGLE_SINGLETONS; - #$this->popApp; - - # Note this does not do much, except for some tests that use it directly. - # The first call to File::Temp caches the temp directory name, so - # this value won't get used for anything created by File::Temp - $cfgData->{TempfileDir} = "$cfgData->{WorkingDir}/requestTmp"; - - # Move logging into a temporary directory - my $logdir = Cwd::getcwd() . '/testlogs'; - $logdir =~ m/^(.*)$/; - $logdir = $1; - $cfgData->{Log}{Dir} = $logdir; - mkdir($logdir) unless -d $logdir; - $cfgData->{Log}{Implementation} = 'Foswiki::Logger::Compatibility'; - $cfgData->{LogFileName} = "$logdir/FoswikiTestCase.log"; - $cfgData->{WarningFileName} = "$logdir/FoswikiTestCase.warn"; - $cfgData->{DebugFileName} = "$logdir/FoswikiTestCase.debug"; - $cfgData->{AdminUserWikiName} = 'AdminUser'; - $cfgData->{AdminUserLogin} = 'root'; - $cfgData->{SuperAdminGroup} = 'AdminGroup'; + $this->setupDirs; + + $this->setupAdminUser; # The unit tests really need CGI sessions or captureWithKey fails $cfgData->{Sessions}{EnableGuestSessions} = 1; @@ -825,7 +659,7 @@ s/((\$Foswiki::cfg\{.*?\})\s*=.*?;)(?:\n|$)/push(@moreConfig, $1) unless (eval " # have been called when the first Foswiki object was created, above.) $this->loadExtraConfig(@_); - _onceOnlyChecks(); + $this->_onceOnlyChecks(); }; @@ -833,25 +667,10 @@ s/((\$Foswiki::cfg\{.*?\})\s*=.*?;)(?:\n|$)/push(@moreConfig, $1) unless (eval " around tear_down => sub { my $orig = shift; my $this = shift; + $this->_clear_tempDir; - $this->app->cfg->data( { eval $this->__FoswikiSafe } ); - foreach my $sym ( keys %ENV ) { - unless ( defined( $this->__EnvSafe->{$sym} ) ) { - delete $ENV{$sym}; - } - else { - $ENV{$sym} = $this->__EnvSafe->{$sym}; - } - } - foreach my $sym ( keys %{ $this->__EnvReset } ) { - if ( defined $this->__EnvReset->{$sym} ) { - $ENV{$sym} = $this->__EnvReset->{$sym}; - } - else { - delete $ENV{$sym}; - } - } + $this->restoreEnvironment; # Clear down non-default META types. foreach my $thing ( keys %$Foswiki::Meta::VALIDATE ) { @@ -924,7 +743,7 @@ Remove a temporary web fixture (data and pub) sub removeWebFixture { my ( $this, $web ) = @_; - ASSERT( !ref( $_[1] ), "Old-style call to removeWebFixture" ); + ASSERT( !ref( $_[1] ), "Non-OO call to removeWebFixture" ); try { my $webObject = $this->create( 'Foswiki::Meta', web => $web ); @@ -1082,51 +901,6 @@ sub getUIFn { =begin TML ----++ ObjectMethod createNewFoswikiApp(%params) -> ref to new Unit::TestApp obj - -cleans up the existing Foswiki object, and creates a new one - -=%params= are passed directly to the =Foswiki::App= constructor. - -typically called to force a full re-initialisation either with new preferences, topics, users, groups or CFG - -=cut - -sub createNewFoswikiApp { - my $this = shift; - - my %params = @_; - - $this->clear_test_topicObject; - $this->clear_request; - ASSERT( !defined $Foswiki::app ) if SINGLE_SINGLETONS; - $Foswiki::cfg{Store}{Implementation} ||= 'Foswiki::Store::PlainFile'; - - $params{env} //= $this->app->cloneEnv; - my $app = Unit::TestApp->new( cfg => $this->app->cfg->clone, %params ); - - $this->app($app); - $this->_fixupAppObjects; - - # WorkDir is set to _tempDir but _tempDir might be cleaned up before $app - # gets completely shutdown. This draws some app frameworks to fail upon - # cleanup as they rely upon WorkDir. By storing the _tempDir object on app's - # heap we let them shutdown cleanly. - $app->heap->{TestCase_TempDir} = $this->_tempDir; - - ASSERT( defined $Foswiki::app ) if SINGLE_SINGLETONS; - - if ( $this->test_web && $this->test_topic ) { - $this->test_topicObject( - ( Foswiki::Func::readTopic( $this->test_web, $this->test_topic ) ) - [0] ); - } - - return $this->app; -} - -=begin TML - ---++ ObjectMethod reCreateFoswikiApp Creates a new app object using currently active one as the template. @@ -1229,59 +1003,29 @@ sub setLocalizableAttributes { } around setLocalizeFlags => sub { - my $orig = shift; - my $flags = $orig->(@_); + my $orig = shift; # Don't clean app on localizing as we might need it until the new one is # created. - $flags->{clearAttributes} = 0; - - return $flags; + return $orig->(@_), clearAttributes => 0; }; -# Correct all Foswiki::AppObject to use currently active Foswiki::App object. -# SMELL Hacky but shall be transparent for any derived test case class. -sub _fixupAppObjects { - my $this = shift; - - my $app = $this->app; - - foreach my $attr ( keys %$this ) { - if ( - blessed( $this->{$attr} ) - && $this->$attr->isa('Foswiki::Object') - && $this->$attr->can('_set_app') - && ( !defined( $this->$attr->app ) - || ( $this->$attr->app != $app ) ) - ) - { - $this->$attr->_set_app($app); - } - } -} - -sub pushApp { - my $this = shift; - my %params; - - my $holderObj = $this->localize(@_); - - push @{ $this->_holderStack }, $holderObj; -} - -sub popApp { +around createNewFoswikiApp => sub { + my $orig = shift; my $this = shift; - ASSERT( @{ $this->_holderStack } > 0, "Empty stack of holder objects" ) - if DEBUG; + $this->clear_test_topicObject; + $this->clear_request; - pop @{ $this->_holderStack }; + my $newApp = $orig->( $this, @_ ); - $Foswiki::app = $this->app; + if ( $this->test_web && $this->test_topic ) { + $this->test_topicObject( + ( $newApp->readTopic( $this->test_web, $this->test_topic ) )[0] ); + } - $this->app->cfg->assignGLOB; - $this->_fixupAppObjects; -} + return $newApp; +}; 1; __DATA__ diff --git a/UnitTestContrib/test/unit/FuncTests.pm b/UnitTestContrib/test/unit/FuncTests.pm index 7438c3127a..10d683706d 100644 --- a/UnitTestContrib/test/unit/FuncTests.pm +++ b/UnitTestContrib/test/unit/FuncTests.pm @@ -135,7 +135,6 @@ around tear_down => sub { unlink $this->tmpdatafile2; $this->removeWebFixture( $this->test_web2 ); $orig->($this); - $| = 0; return; }; @@ -217,7 +216,7 @@ sub test_createWeb_permissions { my $this = shift; $this->app->cfg->data->{EnableHierarchicalWebs} = 1; - $this->pushApp; + $this->saveState; $this->createNewFoswikiApp( engineParams => { @@ -234,7 +233,7 @@ sub test_createWeb_permissions { \t* Set DENYWEBCHANGE = $defaultUserWikiName HERE - $this->popApp; + $this->restoreState; # Verify that create of a root web is denied by default user. try { diff --git a/UnitTestContrib/test/unit/FuncUsersTests.pm b/UnitTestContrib/test/unit/FuncUsersTests.pm index 275145d1da..8f7bf6c630 100644 --- a/UnitTestContrib/test/unit/FuncUsersTests.pm +++ b/UnitTestContrib/test/unit/FuncUsersTests.pm @@ -165,9 +165,9 @@ sub HtPasswordPasswordManager { # See the pod doc in Unit::TestCase for details of how to use this sub fixture_groups { return ( - [ 'NoLoginManager', 'ApacheLoginManager', 'TemplateLoginManager' ], - [ 'AllowLoginName', 'DontAllowLoginName' ], - [ 'NonePasswordManager', 'HtPasswordPasswordManager' ], + [ 'TemplateLoginManager', 'NoLoginManager', 'ApacheLoginManager', ], + [ 'AllowLoginName', 'DontAllowLoginName', ], + [ 'HtPasswordPasswordManager', 'NonePasswordManager', ], ['TopicUserMapping'] ); #TODO: 'BaseUserMapping' } diff --git a/UnitTestContrib/test/unit/ManageDotPmTests.pm b/UnitTestContrib/test/unit/ManageDotPmTests.pm index c550497a14..1963ecd85a 100644 --- a/UnitTestContrib/test/unit/ManageDotPmTests.pm +++ b/UnitTestContrib/test/unit/ManageDotPmTests.pm @@ -44,7 +44,7 @@ around set_up => sub { $this->app->cfg->data->{TrashWebName} = $this->trash_web; @FoswikiFnTestCase::mails = (); - $Foswiki::cfg{Sessions}{TopicsRequireGuestSessions} = + $this->app->cfg->data->{Sessions}{TopicsRequireGuestSessions} = '(WikiGroups|Registration|RegistrationParts|ResetPassword)$'; return; @@ -64,7 +64,6 @@ around tear_down => sub { if ( $store->webExists( $this->test_web . "EmptyNewExtra" ) ); $orig->($this); - $| = 0; return; }; @@ -248,7 +247,8 @@ sub _registerUserException { # Capture the session id for the user we just registered. This is used to confirm # that the deleteUser removes the correct cgisess_ file. - $session_id = Foswiki::Func::getSessionValue('_SESSION_ID'); + $session_id = + $this->app->users->getLoginManager->getSessionValue('_SESSION_ID'); # Reload caches $this->createNewFoswikiApp; @@ -337,9 +337,12 @@ sub test_SingleAddToNewGroupCreate { my $this = shift; my $ret; + say STDERR __PACKAGE__, "::1"; + $ret = $this->registerUserExceptionTwk( 'asdf', 'Asdf', 'Poiu', 'asdf@example.com' ); $this->assert_null( $ret, "Simple rego should work" ); + say STDERR __PACKAGE__, "::10"; $ret = $this->addUserToGroup( { @@ -350,19 +353,23 @@ sub test_SingleAddToNewGroupCreate { } ); $this->assert_null( $ret, "Simple add to new group" ); + say STDERR __PACKAGE__, "::20"; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + say STDERR __PACKAGE__, "::30"; #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); + say STDERR __PACKAGE__, "::40"; #SMELL: (maybe) yes, at the moment, the currently logged in user _is_ also added to the group - this ensures that they are able to complete the operation - as we're saving once per user - $this->assert( - Foswiki::Func::isGroupMember( "NewGroup", $this->app->user ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", $this->app->user ) ); + say STDERR __PACKAGE__, "::41"; + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + say STDERR __PACKAGE__, "::50"; return; } @@ -392,22 +399,21 @@ sub test_DoubleAddToNewGroupCreate { $this->assert_null( $ret, "Simple add to new group" ); - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #SMELL: (maybe) yes, at the moment, the currently logged in user _is_ also added to the group - this ensures that they are able to complete the operation - as we're saving once per user - $this->assert( - Foswiki::Func::isGroupMember( "NewGroup", $this->app->user ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", $this->app->user ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); return; } @@ -445,22 +451,21 @@ sub test_TwiceAddToNewGroupCreate { ); $this->assert_null( $ret, "add myself" ); - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #SMELL: (maybe) yes, at the moment, the currently logged in user _is_ also added to the group - this ensures that they are able to complete the operation - as we're saving once per user - $this->assert( - Foswiki::Func::isGroupMember( "NewGroup", $this->app->user ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", $this->app->user ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); $ret = $this->addUserToGroup( { @@ -472,22 +477,21 @@ sub test_TwiceAddToNewGroupCreate { ); $this->assert_null( $ret, "second add user" ); - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #SMELL: (maybe) yes, at the moment, the currently logged in user _is_ also added to the group - this ensures that they are able to complete the operation - as we're saving once per user - $this->assert( - Foswiki::Func::isGroupMember( "NewGroup", $this->app->user ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", $this->app->user ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); $ret = $this->addUserToGroup( { @@ -500,25 +504,24 @@ sub test_TwiceAddToNewGroupCreate { ); $this->assert_null( $ret, "third add user" ); - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #SMELL: (maybe) yes, at the moment, the currently logged in user _is_ also added to the group - this ensures that they are able to complete the operation - as we're saving once per user - $this->assert( - Foswiki::Func::isGroupMember( "NewGroup", $this->app->user ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "QwerPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu3" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", $this->app->user ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "QwerPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu3" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); $ret = $this->removeUserFromGroup( { @@ -528,13 +531,13 @@ sub test_TwiceAddToNewGroupCreate { } ); $this->assert_null( $ret, "remove one user" ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu3" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu3" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); $ret = $this->removeUserFromGroup( { @@ -544,15 +547,15 @@ sub test_TwiceAddToNewGroupCreate { } ); $this->assert_null( $ret, "remove two user" ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu3" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu2" ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", "ZxcvPoiu3" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "ZxcvPoiu4" ) ); return; } @@ -580,15 +583,15 @@ sub test_SingleAddToNewGroupNoCreate { #SMELL: TopicUserMapping specific - we don't refresh Groups cache :( $this->assert( - !Foswiki::Func::topicExists( $this->users_web, "AnotherNewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + !$this->app->topicExists( $this->users_web, "AnotherNewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; $this->assert( - !Foswiki::Func::topicExists( $this->users_web, "AnotherNewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + !$this->app->topicExists( $this->users_web, "AnotherNewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); return; } @@ -607,16 +610,15 @@ sub test_NoUserAddToNewGroupCreate { ); #SMELL: TopicUserMapping specific - we don't refresh Groups cache :( - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); # If not running as admin, current user is automatically added to the group. - $this->assert( - Foswiki::Func::isGroupMember( "NewGroup", $this->app->user ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", $this->app->user ) ); return; } @@ -639,12 +641,12 @@ sub test_InvalidUserAddToNewGroupCreate { $this->assert_matches( qr/Invalid username/, $ret->{params}[0] ); #SMELL: TopicUserMapping specific - we don't refresh Groups cache :( - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); $ret = $this->addUserToGroup( { @@ -658,7 +660,7 @@ sub test_InvalidUserAddToNewGroupCreate { #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::isGroupMember( "NewGroup", 'Us_aaUser' ) ); + $this->assert( $this->app->isGroupMember( "NewGroup", 'Us_aaUser' ) ); return; } @@ -684,16 +686,16 @@ sub test_NoUserAddToNewGroupCreateAsAdmin { ); #SMELL: TopicUserMapping specific - we don't refresh Groups cache :( - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; - $this->assert( Foswiki::Func::topicExists( $this->users_web, "NewGroup" ) ); + $this->assert( $this->app->topicExists( $this->users_web, "NewGroup" ) ); # If running as admin, no user is automatically added to the group. $this->assert( - !Foswiki::Func::isGroupMember( + !$this->app->isGroupMember( "NewGroup", $this->app->cfg->data->{AdminUserWikiName} ) ); @@ -722,15 +724,15 @@ sub test_RemoveFromNonExistantGroup { #SMELL: TopicUserMapping specific - we don't refresh Groups cache :( $this->assert( - !Foswiki::Func::topicExists( $this->users_web, "AnotherNewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + !$this->app->topicExists( $this->users_web, "AnotherNewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; $this->assert( - !Foswiki::Func::topicExists( $this->users_web, "AnotherNewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + !$this->app->topicExists( $this->users_web, "AnotherNewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); return; } @@ -756,15 +758,15 @@ sub test_RemoveNoUserFromExistantGroup { #SMELL: TopicUserMapping specific - we don't refresh Groups cache :( $this->assert( - !Foswiki::Func::topicExists( $this->users_web, "AnotherNewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + !$this->app->topicExists( $this->users_web, "AnotherNewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); #need to reload to force Foswiki to reparse Groups :( $this->reCreateFoswikiApp; $this->assert( - !Foswiki::Func::topicExists( $this->users_web, "AnotherNewGroup" ) ); - $this->assert( !Foswiki::Func::isGroupMember( "NewGroup", "AsdfPoiu" ) ); + !$this->app->topicExists( $this->users_web, "AnotherNewGroup" ) ); + $this->assert( !$this->app->isGroupMember( "NewGroup", "AsdfPoiu" ) ); return; } @@ -852,7 +854,7 @@ sub verify_bulkRegister { $cfgData->{MinPasswordLength} = 2; my ($topicObject) = - Foswiki::Func::readTopic( $this->users_web, 'NewUserTemplate' ); + $this->app->readTopic( $this->users_web, 'NewUserTemplate' ); $topicObject->text(<<'EOF'); %NOP{Ignore this}% Default user template @@ -866,7 +868,7 @@ AFTER EOF $topicObject->save(); - Foswiki::Func::saveTopic( $this->users_web, 'AltUserTemplate', undef, + $this->app->saveTopic( $this->users_web, 'AltUserTemplate', undef, <<'EOF2' ); %NOP{Ignore this}% Alternate user template @@ -957,12 +959,12 @@ EOM #print STDERR Data::Dumper::Dumper( \$stdout ); #print STDERR Data::Dumper::Dumper( \$stderr ); - $this->assert( Foswiki::Func::topicExists( $this->test_web, $regTopic ) ); + $this->assert( $this->app->topicExists( $this->test_web, $regTopic ) ); # SMELL: The registration log is created in UsersWeb, and not in the web containing the # list of users to be registered. Needs some thought. $this->assert( - Foswiki::Func::topicExists( + $this->app->topicExists( $this->app->cfg->data->{UsersWebName}, $logTopic ) ); @@ -1002,11 +1004,13 @@ qr/You cannot register twice, the name 'TestBulkUser4' is already registered\./, foreach my $wikiname (@expected) { print STDERR "TESTING $wikiname\n"; $this->assert( - Foswiki::Func::topicExists( + $this->app->topicExists( $this->app->cfg->data->{UsersWebName}, $wikiname ), "Missing $wikiname" ); + + # SMELL Use of deprecated API my $utext = Foswiki::Func::readTopicText( $this->app->cfg->data->{UsersWebName}, $wikiname ); @@ -1170,7 +1174,7 @@ sub verify_deleteUserAsAdmin { -e $this->app->cfg->data->{WorkingDir} . "/tmp/cgisess_$session_id" ); $this->assert( - Foswiki::Func::addUserToGroup( + $this->app->addUserToGroup( $this->new_user_wikiname, $this->new_user_wikiname . 'Group', 1 ) ); @@ -1236,7 +1240,7 @@ qr/user removed from Mapping Manager.*removed cgisess_${session_id}.*user remove !-e $this->app->cfg->data->{WorkingDir} . "/tmp/cgisess_$session_id" ); $this->assert( - !Foswiki::Func::isGroupMember( + !$this->app->isGroupMember( $this->new_user_wikiname, $this->new_user_wikiname . 'Group' ) ); @@ -1251,7 +1255,7 @@ qr/user removed from Mapping Manager.*removed cgisess_${session_id}.*user remove if $this->app->cfg->data->{Register}{AllowLoginName}; $this->assert_null(`grep $new_user_wikiname $htpasswdFile`); - my ( $crap, $wu ) = Foswiki::Func::readTopic( + my ( $crap, $wu ) = $this->app->readTopic( $this->app->cfg->data->{UsersWebName}, $this->app->cfg->data->{UsersTopicName} ); @@ -1259,7 +1263,7 @@ qr/user removed from Mapping Manager.*removed cgisess_${session_id}.*user remove $this->assert( $wu !~ /$new_user_login/s ); $this->assert( - !Foswiki::Func::topicExists( + !$this->app->topicExists( $this->app->cfg->data->{UsersWebName}, $this->new_user_wikiname ) @@ -1276,7 +1280,7 @@ sub verify_deleteUserWithPrefix { $this->new_user_login('eric'); $this->assert( - Foswiki::Func::addUserToGroup( + $this->app->addUserToGroup( $this->new_user_wikiname, $this->new_user_wikiname . 'Group', 1 ) ); @@ -1391,7 +1395,7 @@ qr/user removed from Mapping Manager.*removed cgisess_${session_id}.*user remove }; $this->assert( - !Foswiki::Func::isGroupMember( + !$this->app->isGroupMember( $this->new_user_wikiname, $this->new_user_wikiname . 'Group' ) ); @@ -1407,7 +1411,7 @@ qr/user removed from Mapping Manager.*removed cgisess_${session_id}.*user remove if $this->app->cfg->data->{Register}{AllowLoginName}; $this->assert_null(`grep $new_user_wikiname $htpasswdFile`); - my ( $crap, $wu ) = Foswiki::Func::readTopic( + my ( $crap, $wu ) = $this->app->readTopic( $this->app->cfg->data->{UsersWebName}, $this->app->cfg->data->{UsersTopicName} ); @@ -1415,7 +1419,7 @@ qr/user removed from Mapping Manager.*removed cgisess_${session_id}.*user remove $this->assert( $wu !~ /$new_user_login/s ); $this->assert( - !Foswiki::Func::topicExists( + !$this->app->topicExists( $this->app->cfg->data->{UsersWebName}, $this->new_user_wikiname ) @@ -1480,14 +1484,14 @@ sub test_createDefaultWeb { undef $webObject; #check that the topics from _default web are actually in the new web, and make sure they are expectently similar - my @expectedTopicsItr = Foswiki::Func::getTopicList('_default'); + my @expectedTopicsItr = $this->app->getTopicList('_default'); foreach my $expectedTopic (@expectedTopicsItr) { - $this->assert( Foswiki::Func::topicExists( $newWeb, $expectedTopic ) ); + $this->assert( $this->app->topicExists( $newWeb, $expectedTopic ) ); my ( $eMeta, $eText ) = - Foswiki::Func::readTopic( '_default', $expectedTopic ); + $this->app->readTopic( '_default', $expectedTopic ); undef $eMeta; my ( $nMeta, $nText ) = - Foswiki::Func::readTopic( $newWeb, $expectedTopic ); + $this->app->readTopic( $newWeb, $expectedTopic ); undef $nMeta; #change the params set above to what they were in the template WebPreferences @@ -1511,8 +1515,7 @@ sub test_saveSettings_allowed { my $this = shift; # Create a test topic - my ($testTopic) = - Foswiki::Func::readTopic( $this->test_web, "SaveSettings" ); + my ($testTopic) = $this->app->readTopic( $this->test_web, "SaveSettings" ); $testTopic->text( <<'TEXT'); Philosophers, philosophers, everywhere, * Set TEXTSET = text set @@ -1571,7 +1574,7 @@ TEXT $this->assert_equals( "new set", $prefs->getPreference('NEWSET') ); $this->assert_equals( "new local", $prefs->getPreference('NEWLOCAL') ); my ( $tdate, $tuser, $trev, $tcomment ) = - Foswiki::Func::getRevisionInfo( $this->test_web, 'SaveSettings' ); + $this->app->getRevisionInfo( $this->test_web, 'SaveSettings' ); $this->assert_equals( 2, $trev ); return; @@ -1582,8 +1585,7 @@ sub test_saveSettings_denied { my $this = shift; # Create a test topic - my ($testTopic) = - Foswiki::Func::readTopic( $this->test_web, "SaveSettings" ); + my ($testTopic) = $this->app->readTopic( $this->test_web, "SaveSettings" ); $testTopic->text(<<'TEXT'); Philosophers, philosophers, everywhere, * Set ALLOWTOPICCHANGE = ZeusAndHera @@ -1648,7 +1650,7 @@ TEXT $this->assert_null( $prefs->getPreference('NEWSET') ); $this->assert_null( $prefs->getPreference('NEWLOCAL') ); my ( $tdate, $tuser, $trev, $tcomment ) = - Foswiki::Func::getRevisionInfo( $this->test_web, 'SaveSettings' ); + $this->app->getRevisionInfo( $this->test_web, 'SaveSettings' ); $this->assert_equals( 1, $trev ); return; @@ -1658,8 +1660,7 @@ sub test_saveSettings_cancel { my $this = shift; # Create a test topic - my ($testTopic) = - Foswiki::Func::readTopic( $this->test_web, "SaveSettings" ); + my ($testTopic) = $this->app->readTopic( $this->test_web, "SaveSettings" ); $testTopic->text( <<'TEXT'); Philosophers, philosophers, everywhere, * Set TEXTSET = text set @@ -1718,7 +1719,7 @@ TEXT $this->assert_equals( "meta set", $prefs->getPreference('METASET') ); $this->assert_equals( "meta local", $prefs->getPreference('METALOCAL') ); my ( $tdate, $tuser, $trev, $tcomment ) = - Foswiki::Func::getRevisionInfo( $this->test_web, 'SaveSettings' ); + $this->app->getRevisionInfo( $this->test_web, 'SaveSettings' ); $this->assert_equals( 1, $trev ); return; @@ -1728,8 +1729,7 @@ sub test_saveSettings_invalid { my $this = shift; # Create a test topic - my ($testTopic) = - Foswiki::Func::readTopic( $this->test_web, "SaveSettings" ); + my ($testTopic) = $this->app->readTopic( $this->test_web, "SaveSettings" ); $testTopic->text( <<'TEXT'); Philosophers, philosophers, everywhere, * Set TEXTSET = text set @@ -1797,7 +1797,7 @@ TEXT $this->assert_equals( "meta set", $prefs->getPreference('METASET') ); $this->assert_equals( "meta local", $prefs->getPreference('METALOCAL') ); my ( $tdate, $tuser, $trev, $tcomment ) = - Foswiki::Func::getRevisionInfo( $this->test_web, 'SaveSettings' ); + $this->app->getRevisionInfo( $this->test_web, 'SaveSettings' ); $this->assert_equals( 1, $trev ); return; @@ -1871,19 +1871,19 @@ sub test_createEmptyWeb { #$this->assert_equals('on', $webObject->getPreference('SITEMAPLIST')); #check that the topics from _default web are actually in the new web, and make sure they are expectently similar - my @expectedTopicsItr = Foswiki::Func::getTopicList('_empty'); + my @expectedTopicsItr = $this->app->getTopicList('_empty'); foreach my $expectedTopic (@expectedTopicsItr) { - $this->assert( Foswiki::Func::topicExists( $newWeb, $expectedTopic ) ); + $this->assert( $this->app->topicExists( $newWeb, $expectedTopic ) ); next if ( $expectedTopic eq 'WebPreferences' ) ; # we've modified the topic alot my ( $eMeta, $eText ) = - Foswiki::Func::readTopic( '_empty', $expectedTopic ); + $this->app->readTopic( '_empty', $expectedTopic ); undef $eMeta; my ( $nMeta, $nText ) = - Foswiki::Func::readTopic( $newWeb, $expectedTopic ); + $this->app->readTopic( $newWeb, $expectedTopic ); undef $nMeta; #change the params set above to what they were in the template WebPreferences diff --git a/UnitTestContrib/test/unit/PasswordTests.pm b/UnitTestContrib/test/unit/PasswordTests.pm index 97adba4f4e..0a67d6ab0b 100644 --- a/UnitTestContrib/test/unit/PasswordTests.pm +++ b/UnitTestContrib/test/unit/PasswordTests.pm @@ -27,8 +27,6 @@ around set_up => sub { my $orig = shift; my $this = shift; - $| = 1; - $orig->( $this, @_ ); $this->createNewFoswikiApp; @@ -100,8 +98,6 @@ around tear_down => sub { $orig->( $this, @_ ); - $| = 0; - return; }; diff --git a/UnitTestContrib/test/unit/PlackPostTests.pm b/UnitTestContrib/test/unit/PlackPostTests.pm new file mode 100644 index 0000000000..d43b7feb23 --- /dev/null +++ b/UnitTestContrib/test/unit/PlackPostTests.pm @@ -0,0 +1,181 @@ +# See bottom of file for license and copyright information + +package PlackPostTests; +use v5.14; +use utf8; + +use HTTP::Request::Common; +use HTML::Parser (); +use Data::Dumper; +use Encoding; + +use Foswiki::Class; +extends qw(Unit::PlackTestCase); + +use constant SIMPLE_CONTENT => + "Simple Text File\nАбо просто текст у файлі\n"; + +around prepareTestClientList => sub { + my $orig = shift; + my $this = shift; + my $tests = $orig->( $this, @_ ); + + push @$tests, ( + { + name => 'attach_simple', + client => \&_test_attach_simple, + appParams => { env => { FOSWIKI_TEST_NOP => 'Simple env test', }, }, + testWebs => { + $this->testWebName('AttachSimple') => + { TopicForAttach => 'Some topic text', }, + }, + testUsers => [ + { + login => 'user1', + forename => 'User1', + surname => 'SurUser1', + email => 'user1@example.com', + group => 'TestGroup', + }, + { + login => 'user2', + forename => 'User2', + surname => 'SurUser2', + email => 'user2@example.com', + group => 'TestGroup', + }, + ], + initRequest => sub { + my $this = shift; + $this->app->cfg->data->{Validation}{Method} = 'none'; + $this->app->cfg->data->{DisableAllPlugins} = 1; + }, + }, + ); + return $tests; +}; + +sub _test_attach_simple { + my $this = shift; + my %args = @_; + + my $app = $this->app; + my $test = $args{testObject}; + + my $web = ( keys %{ $args{testParams}{testWebs} } )[0]; + my $topic = ( keys %{ $args{testParams}{testWebs}{$web} } )[0]; + + my $viewUrl = $app->getScriptUrlPath( $web, $topic, "view" ); + + my $res = $test->request( GET $viewUrl); + my $content = $res->content; + + my $matchedA = $this->findHTMLTag( + $content, + tag => 'a', + text => 'Attach', + class => qr/^foswikiReq/, + ); + + $this->assert( defined($matchedA), "Attach link not found in output" ); + + my $attachUrl = $matchedA->{attrs}{href}; + $res = $test->request( GET $attachUrl); + $content = $res->content; + + my $attachForm = $this->findHTMLTag( + $content, + tag => 'form', + text => qr/Attach new file/, + name => 'main', + ); + + $this->assert( defined($attachForm), + "Cannot find attachment form in response to $attachUrl request" ); + + my $method = $attachForm->{attrs}{method}; + + $this->assert_equals( 'post', lc($method), + "Expected form method POST but received $method" ); + + my $contentEnc = $attachForm->{attrs}{enctype}; + my $uploadUrl = $attachForm->{attrs}{action}; + + my $attachFileName = "TestAttachFile.txt"; + + $res = $test->request( + POST $uploadUrl, + Content_Type => 'form-data', + Content_Encoding => $contentEnc, + Content => [ + attach => [ + undef, $attachFileName, + Content => Encode::encode( 'utf-8', SIMPLE_CONTENT ), + ], + ], + ); + + $this->assert( $res->is_redirect, + "Upload must have finished with redirect response." ); + + my $redirUrl = $res->header('Location'); + + $this->assert_equals( $viewUrl, $redirUrl, + "Redirect doesn't point back to $viewUrl" ); + + $res = $test->request( GET $redirUrl); + + $this->assert( $res->code == 200, "Bad request status code " . $res->code ); + + $content = $res->content; + + my $attachmentsTable = $this->findHTMLTag( + $content, + tag => 'table', + class => qr/foswikiTable/, + text => $attachFileName, + ); + + $this->assert( defined($attachmentsTable), + "The topic " + . $web . "." + . $topic + . " doesn't contain attachments table with test attachment " + . $attachFileName ); + + my $filePath = File::Spec->catfile( $this->app->cfg->data->{PubDir}, + $web, $topic, $attachFileName ); + + my $fh; + $this->assert( + open( $fh, "<:encoding(utf8)", $filePath ), + "Cannot open $filePath for reading: $!" + ); + + local $/; + my $fileContent = <$fh>; + close $fh; + + $this->assert_equals( SIMPLE_CONTENT, $fileContent, + "Actual attachment content differs from what's been uploaded" ); +} + +1; +__END__ +Foswiki - The Free and Open Source Wiki, http://foswiki.org/ + +Copyright (C) 2016 Foswiki Contributors. Foswiki Contributors +are listed in the AUTHORS file in the root of this distribution. +NOTE: Please extend that file, not this notice. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. For +more details read LICENSE in the root of this distribution. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +As per the GPL, removal of this notice is prohibited. diff --git a/UnitTestContrib/test/unit/PlackViewTests.pm b/UnitTestContrib/test/unit/PlackViewTests.pm index 4eb271a328..9ef2c52e7b 100644 --- a/UnitTestContrib/test/unit/PlackViewTests.pm +++ b/UnitTestContrib/test/unit/PlackViewTests.pm @@ -6,17 +6,10 @@ use v5.14; use Assert; use HTTP::Request::Common; -use Moo; +use Foswiki::Class; use namespace::clean; extends qw(Unit::PlackTestCase); -around initialize => sub { - my $orig = shift; - my $this = shift; - - $orig->( $this, @_ ); -}; - around prepareTestClientList => sub { my $orig = shift; my $this = shift; @@ -42,7 +35,9 @@ around prepareTestClientList => sub { sub client_simple { my $this = shift; - my $test = shift; + my %args = @_; + + my $test = $args{testObject}; my $expected = '

Welcome to the Main web

diff --git a/UnitTestContrib/test/unit/PluginHandlerTests.pm b/UnitTestContrib/test/unit/PluginHandlerTests.pm index c5d155deaf..bbdffca68d 100644 --- a/UnitTestContrib/test/unit/PluginHandlerTests.pm +++ b/UnitTestContrib/test/unit/PluginHandlerTests.pm @@ -1058,7 +1058,7 @@ HERE sub test_finishPlugin { my $this = shift; - $this->pushApp; + $this->saveState; $this->reCreateFoswikiApp; $this->makePlugin( 'finishPlugin', <<'HERE'); sub finishPlugin { @@ -1068,7 +1068,7 @@ HERE $this->finishFoswikiSession(); $this->checkCalls( 1, 'finishPlugin' ); - $this->popApp; + $this->restoreState; return; } diff --git a/core/lib/Foswiki/App.pm b/core/lib/Foswiki/App.pm index d23e9b9f7d..0e24342cc2 100644 --- a/core/lib/Foswiki/App.pm +++ b/core/lib/Foswiki/App.pm @@ -1,5 +1,8 @@ # See bottom of file for license and copyright information +package Foswiki::App; +use v5.14; + =begin TML ---+!! Class Foswiki::App @@ -9,16 +12,12 @@ functionality. =cut -package Foswiki::App; -use v5.14; - use constant TRACE_REQUEST => 0; use Assert; use Cwd; use Try::Tiny; use Storable qw(dclone); -use Foswiki qw(%regex); # SMELL CGI is only used for generating a simple error page using HTML tags # shortcut functions. Must be replaced with something more reasonable. @@ -30,10 +29,9 @@ use Foswiki::Exception; use Foswiki::Sandbox; use Foswiki::WebFilter; use Foswiki::Time; -use Foswiki qw(load_package load_class isTrue); +use Foswiki qw(%regex load_package load_class isTrue); use Foswiki::Class qw(callbacks); -use namespace::clean; extends qw(Foswiki::Object); callback_names qw(handleRequestException postConfig); @@ -253,10 +251,18 @@ has remoteUser => ( return $this->users->loadSession( $this->engine->user ); }, ); + +# XXX user attribute must have complicated options like lazy or builder. +# Otherwise there is high risk of Perl or Moo optimizing RO-calls to this +# attribute leading to corrupt stack if passed over as a method attribute and +# then set to a different value deep in the call stack. For example if there is +# a call: $app->addUserToGroup($app->user, ...) - it may cause corrupt stack +# because TopicUserMapping temporarily escalates current user to Admin +# privileges. has user => ( - is => 'rw', - clearer => 1, - predicate => 1, + is => 'rw', + lazy => 1, + builder => '_prepareUser', ); has users => ( is => 'rw', @@ -318,6 +324,8 @@ sub BUILD { $this->cfg->bootstrapSystemSettings; } + $this->callback('postConfig'); + my $cfgData = $this->cfg->data; if ( $cfgData->{Store}{overrideUmask} && $cfgData->{OS} ne 'WINDOWS' ) { @@ -398,24 +406,6 @@ sub DEMOLISH { my $this = shift; my ($in_global) = @_; - # Clean up sessions before we finish. - if ( 0 && DEBUG ) { - if ($in_global) { - say STDERR ">>>>"; - say STDERR Carp::longmess( ref($this) . '::DEMOLISH' ); - say STDERR "Object from ", $this->{__orig_file}, ":", - $this->{__orig_line}; - say STDERR $this->{__orig_stack}; - say STDERR "<<<<"; - require Devel::MAT::Dumper; - } - else { - say STDERR ref($this) . '::DEMOLISH'; - say STDERR "Object from ", $this->{__orig_file}, ":", - $this->{__orig_line}; - say STDERR $this->{__orig_stack}; - } - } $this->users->loginManager->complete; } @@ -446,9 +436,9 @@ sub run { my ( $app, $rc ); # We use shell environment by default. PSGI would supply its own env - # hashref. Because PSGI env is not the same as shell env we would need to - # avoid any side effects related to situations when changes to the env - # hashref are gettin' translated back onto the shell env. + # hashref. Because PSGI env is not the same as shell env we must clone the + # latter in order to avoid any side effects related to situations when + # changes to the env hashref are gettin' translated back onto the shell env. $params{env} //= dclone( \%ENV ); # Use current working dir for fetching the initial setlib.cfg @@ -1198,6 +1188,8 @@ sub systemMessage { if (@_) { push @{ $this->system_messages }, @_; } + + # SMELL Something better than %BR% shall be used here. return join( '%BR%', @{ $this->system_messages } ); } @@ -1496,7 +1488,6 @@ sub _prepareRequest { sub _prepareConfig { my $this = shift; my $cfg = $this->create('Foswiki::Config'); - $this->callback('postConfig'); return $cfg; } @@ -1539,6 +1530,13 @@ sub _prepareDispatcher { $this->_dispatcherAttrs($dispatcher); } +sub _prepareUser { + my $this = shift; + + #return $this->users->initialiseUser( $this->remoteUser ); + return undef; +} + # If the X-Foswiki-Tickle header is present, this request is an attempt to # verify that the requested function is available on this Foswiki. Respond with # the serialised dispatcher, and finish the request. Need to stringify since @@ -2117,8 +2115,7 @@ Test if logged in user is a guest (WikiGuest) sub isGuest { my $this = shift; - return $this->user eq - $this->users->getCanonicalUserID( $this->cfg->data->{DefaultUserLogin} ); + return $this->users->isGuest( $this->getCanonicalUserID( $this->user ) ); } =begin TML diff --git a/core/lib/Foswiki/AppObject.pm b/core/lib/Foswiki/AppObject.pm index 0e5db8afb1..7daa678d54 100644 --- a/core/lib/Foswiki/AppObject.pm +++ b/core/lib/Foswiki/AppObject.pm @@ -19,6 +19,8 @@ Method create() is imported from Foswiki::App class. use Assert; use Foswiki::Exception; +require Foswiki::Object; + use Moo::Role; has app => ( diff --git a/core/lib/Foswiki/Aux/Localize.pm b/core/lib/Foswiki/Aux/Localize.pm index 7e74fe8ad5..66f9d33ef4 100644 --- a/core/lib/Foswiki/Aux/Localize.pm +++ b/core/lib/Foswiki/Aux/Localize.pm @@ -81,7 +81,8 @@ Have one of the following values: has _localizeState => ( is => 'rw', lazy => 1, clearer => 1, default => '', ); -has _localizeFlags => ( is => 'rw', lazy => 1, builder => 'setLocalizeFlags', ); +has _localizeFlags => + ( is => 'rw', lazy => 1, builder => '_setLocalizeFlags', ); # Removes attribute from the object. sub _clearAttrs { @@ -208,7 +209,11 @@ sub doRestore { } sub setLocalizeFlags { - return { clearAttributes => 1, }; + return clearAttributes => 1; +} + +sub _setLocalizeFlags { + return { $_[0]->setLocalizeFlags }; } =begin TML diff --git a/core/lib/Foswiki/Class.pm b/core/lib/Foswiki/Class.pm index 1f1cacdead..3a880b9db4 100644 --- a/core/lib/Foswiki/Class.pm +++ b/core/lib/Foswiki/Class.pm @@ -1,5 +1,8 @@ # See bottom of file for license and copyright information +package Foswiki::Class; +use v5.14; + =begin TML ---+!! Module Foswiki::Class @@ -69,42 +72,71 @@ manually by the class using =with=. =cut -package Foswiki::Class; -use v5.14; +# Naming conventions for this module: +# _install_something – functions that install feature `something' into the target module; +# _handler_someword - function which implements exported keyword `someword' use Carp; +require Moo::Role; require Moo; +require namespace::clean; +use B::Hooks::EndOfScope 'on_scope_end'; + our @ISA = qw(Moo); +my %_assignedRoles; + sub import { my ($class) = shift; my $target = caller; # Define options we would provide for classes. - my %options = ( callbacks => 0, ); + my %options = ( + callbacks => { + use => 0, + + # Keywords exported with this option. + keywords => [qw(callback_names)], + }, + app => { use => 0, }, + ); my @p; + my @noNsClean = qw(meta); while (@_) { my $param = shift; if ( exists $options{$param} ) { - $options{$param} = 1; + my $opt = $options{$param}; + $opt->{use} = 1; + push @noNsClean, @{ $opt->{keywords} } if defined $opt->{keywords}; } else { push @p, $param; } } - foreach my $option ( keys %options ) { + foreach my $option ( grep { $options{$_}{use} } keys %options ) { my $installer = __PACKAGE__->can("_install_$option"); + die "INTERNAL:There is no installer for option $option" + unless defined $installer; $installer->( $class, $target ); } + on_scope_end { + $class->_apply_roles; + }; + + namespace::clean->import( + -cleanee => $target, + -except => \@noNsClean, + ); + @_ = ( $class, @p ); goto &Moo::import; } -# Actually we're duplicating Moo::_install_coderef here in way. But we better +# Actually we're duplicating Moo::_install_coderef here in a way. But we better # avoid using a module's internalls. sub _inject_code { my ( $name, $code ) = @_; @@ -114,26 +146,38 @@ sub _inject_code { use strict "refs"; } -sub _assign_role { - my ( $target, $role ) = @_; - unless ( $target->does($role) ) { - eval "package $target; use Moo; with qw($role); 1;"; - Carp::confess "Cannot assign role $role to $target: $@" if $@; +sub _apply_roles { + my $class = shift; + foreach my $target ( keys %_assignedRoles ) { + Moo::Role->apply_roles_to_package( $target, + @{ $_assignedRoles{$target} } ); + $class->_maybe_reset_handlemoose($target); + delete $_assignedRoles{$target}; } } +sub _assign_role { + my ( $class, $role ) = @_; + push @{ $_assignedRoles{$class} }, $role; +} + sub _install_callbacks { my ( $class, $target ) = @_; + _assign_role( $target, 'Foswiki::Aux::Callbacks' ); _inject_code( "${target}::callback_names", \&_handler_callbacks ); } sub _handler_callbacks { my $target = caller; - _assign_role( $target, 'Foswiki::Aux::Callbacks' ); Foswiki::Aux::Callbacks::registerCallbackNames( $target, @_ ); } +sub _install_app { + my ( $class, $target ) = @_; + _assign_role( $target, 'Foswiki::AppObject' ); +} + 1; __END__ Foswiki - The Free and Open Source Wiki, http://foswiki.org/ diff --git a/core/lib/Foswiki/Config.pm b/core/lib/Foswiki/Config.pm index ee35a05707..8e9fe00a13 100644 --- a/core/lib/Foswiki/Config.pm +++ b/core/lib/Foswiki/Config.pm @@ -1,5 +1,8 @@ # See bottom of file for license and copyright information +package Foswiki::Config; +use v5.14; + =begin TML ---+!! package Foswiki::Config @@ -8,9 +11,6 @@ Class representing configuration data. =cut -package Foswiki::Config; -use v5.14; - use Assert; use Encode (); use File::Basename; @@ -22,10 +22,8 @@ use Try::Tiny; use Foswiki qw(urlEncode urlDecode make_params); use Foswiki::Configure::FileUtil; -use Moo; -use namespace::clean; +use Foswiki::Class qw(app); extends qw(Foswiki::Object); -with qw(Foswiki::AppObject); with qw(Foswiki::Aux::Localize); # Enable to trace auto-configuration (Bootstrap) diff --git a/core/lib/Foswiki/Exception.pm b/core/lib/Foswiki/Exception.pm index e43fbad546..7387780ef2 100644 --- a/core/lib/Foswiki/Exception.pm +++ b/core/lib/Foswiki/Exception.pm @@ -1,5 +1,8 @@ # See bottom of file for license and copyright information +package Foswiki::Exception; +use v5.14; + =begin TML ---+ package Foswiki::Exception @@ -44,16 +47,13 @@ One more alternative is =CPAN:TryCatch= but it is not found neither in MacPorts, nor in Ubuntu 15.10 repository, nor in CentOS. Though it is a part of FreeBSD ports tree. =cut -package Foswiki::Exception; -use v5.14; - use Assert; use Data::Dumper; use Try::Tiny; use Carp (); use Scalar::Util (); -use Moo; +use Foswiki::Class; use namespace::clean; extends qw(Foswiki::Object); with 'Throwable'; @@ -100,12 +100,12 @@ has line => ( ---++ ObjectAttribute text Simple text explaining what's went wrong. Must always be set to something -meaningful. If child class doesn't expect this attribute to be set by a user -then it must generate it using other attributes. +meaningful. If a child class doesn't expect this attribute to be set by the code +throwing the exception then it must generate it using other attributes. =cut -has text => ( is => 'rwp', ); +has text => ( is => 'rwp', lazy => 1, builder => 'prepareText', ); =begin TML @@ -384,20 +384,33 @@ sub errorStr { return $str; } +# A child exception class must override this method if text can be autogenerated +# from other exception data. +sub prepareText { + my $this = shift; + return "text attribute hasn't been set"; +} + package Foswiki::Exception::Harmless; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception); # For informational exceptions. package Foswiki::Exception::Fatal; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception); +sub BUILD { + my $this = shift; + + say STDERR $this->stacktrace; +} + # To cover perl/system errors. package Foswiki::Exception::ASSERT; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception::Fatal); # This class is only for distinguishing ASSERT-generated exceptions. @@ -410,8 +423,34 @@ Root of callback support exception tree. =cut +package Foswiki::Exception::FileOp; +use Foswiki::Class; +extends qw(Foswiki::Exception::Fatal); + +has file => ( is => 'rw', required => 1, ); +has op => ( is => 'rw', required => 1, ); + +around stringify => sub { + my $orig = shift; + my $this = shift; + + return + "Failed to " + . $this->op + . " file " + . $this->file . ": " + . $orig->($this); +}; + +around prepareText => sub { + my $orig = shift; + my $this = shift; + + return $!; # text attribute to be set from last file operation error. +}; + package Foswiki::Exception::CB; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception); =begin TML @@ -424,7 +463,7 @@ execution chain. =cut package Foswiki::Exception::CB::Last; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception::CB); =begin TML @@ -449,7 +488,7 @@ If configuration key doesn't pass validation. =cut package Foswiki::Exception::Cfg::InvalidKeyName; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception::Fatal); has keyName => ( is => 'rw', required => 1, ); @@ -469,7 +508,7 @@ Attributes: =cut package Foswiki::Exception::HTTPResponse; -use Moo; +use Foswiki::Class; use namespace::clean; extends qw(Foswiki::Exception); @@ -522,7 +561,7 @@ package Foswiki::Exception::HTTPError; use CGI (); use Assert; -use Moo; +use Foswiki::Class; use namespace::clean; extends qw(Foswiki::Exception::HTTPResponse); @@ -566,7 +605,7 @@ Attributes: =cut package Foswiki::Exception::Engine; -use Moo; +use Foswiki::Class; extends qw(Foswiki::Exception::HTTPError); around BUILDARGS => sub { diff --git a/core/lib/Foswiki/Meta.pm b/core/lib/Foswiki/Meta.pm index e3c5e98d81..8180dc1f90 100644 --- a/core/lib/Foswiki/Meta.pm +++ b/core/lib/Foswiki/Meta.pm @@ -114,10 +114,8 @@ use Encode (); use Foswiki::Serialise (); -use Moo; -use namespace::clean; +use Foswiki::Class qw(app); extends qw(Foswiki::Object); -with qw(Foswiki::AppObject); #use Foswiki::Iterator::NumberRangeIterator; @@ -443,45 +441,39 @@ sub registerMETA { ############# GENERIC METHODS ############# around BUILDARGS => sub { - my $orig = shift; - my $class = shift; - my ( $app, $web, $topic, $text ) = @_; - - my %params; - if ( @_ % 2 == 0 ) { - - # Check if we've got key/value pair profile with app key pointing at - # another Meta object. - %params = @_; - if ( defined $params{app} - && $params{app}->isa('Foswiki::Meta') ) - { - ASSERT( !defined( $params{web} ) - && !defined( $params{topic} ) - && !defined( $params{text} ) ); - my $sourceMeta = $params{app}; - $params{app} = $sourceMeta->app; - $params{web} = $sourceMeta->web; - $params{topic} = $sourceMeta->topic; - } - } + my $orig = shift; + my $class = shift; + my %params = @_; - # If by this point there is no valid app key in the parameters then we - # deal with positional parameters. - my $paramHash; - unless ( defined $params{app} && $params{app}->isa('Foswiki::App') ) { + $class ||= ref($class); - # Let the base BUILDARGS deal with those. - $paramHash = $orig->( $class, @_ ); - } - else { - $paramHash = {%params}; + #my ( $app, $web, $topic, $text ) = @_; + + # Check if we've got key/value pair profile with app key pointing at + # another Meta object. + if ( defined $params{app} + && $params{app}->isa('Foswiki::Meta') ) + { + ASSERT( + !defined( $params{web} ) + && !defined( $params{topic} ) + && !defined( $params{text} ), + "Initialization of a new " + . $class + . " object from another object of " + . __PACKAGE__ + . " type must not define neither web, nor topic, nor text attributes." + ); + my $sourceMeta = $params{app}; + $params{app} = $sourceMeta->app; + $params{web} = $sourceMeta->web; + $params{topic} = $sourceMeta->topic; } - delete $paramHash->{web} unless defined $paramHash->{web}; - delete $paramHash->{topic} unless defined $paramHash->{topic}; + delete $params{web} unless defined $params{web}; + delete $params{topic} unless defined $params{topic}; - return $paramHash; + return $orig->( $class, %params ); }; =begin TML @@ -509,6 +501,9 @@ prototype object (which must be type Foswiki::Meta). sub BUILD { my $this = shift; + ASSERT( $this->does('Foswiki::AppObject'), "No AppObject role!" ); + ASSERT( $this->can('app'), "No app method!" ); + # Note: internal fields are prepended with _. All uppercase # fields will be assumed to be meta-data. @@ -3022,12 +3017,18 @@ sub attach { while ( $r = sysread( $opts{stream}, $transfer, 0x80000 ) ) { if ( !defined $r ) { next if ( $! == Errno::EINTR ); - die "system read error: $!\n"; + Foswiki::Exception::FileOp->throw( + file => "(a temporary handle)", + op => "sysread" + ); } my $offset = 0; while ($r) { my $w = syswrite( $fh, $transfer, $r, $offset ); - die "system write error: $!\n" unless ( defined $w ); + Foswiki::Exception::FileOp->throw( + file => "(a temporary handle)", + op => "syswrite", + ) unless ( defined $w ); $offset += $w; $r -= $w; } @@ -3045,7 +3046,10 @@ sub attach { # Have to assume it's changed, even if it hasn't. open( $attrs->{stream}, '<', $attrs->{tmpFilename} ) - || die "Internal error: $!"; + || Foswiki::Exception::FileOp( + file => $attrs->{tmpFilename}, + op => "open" + ); binmode( $attrs->{stream} ); $opts{stream} = $attrs->{stream}; diff --git a/core/lib/Foswiki/MetaCache.pm b/core/lib/Foswiki/MetaCache.pm index 87eb4ee38d..db4ec03482 100644 --- a/core/lib/Foswiki/MetaCache.pm +++ b/core/lib/Foswiki/MetaCache.pm @@ -22,13 +22,8 @@ use Foswiki::Func (); use Foswiki::Meta (); use Foswiki::Users::BaseUserMapping (); -use Moo; -use namespace::clean; +use Foswiki::Class qw(app); extends qw(Foswiki::Object); -with qw(Foswiki::AppObject); - -#use Monitor (); -#Monitor::MonitorMethod('Foswiki::MetaCache', 'getTopicListIterator'); use constant TRACE => 0; diff --git a/core/lib/Foswiki/Net.pm b/core/lib/Foswiki/Net.pm index fd43966f76..4b5fe40333 100644 --- a/core/lib/Foswiki/Net.pm +++ b/core/lib/Foswiki/Net.pm @@ -535,7 +535,7 @@ sub sendEmail { $retries = 0; } catch { - my $msg = ref($_) ? $_->stringify() : $_; + my $msg = Foswiki::Exception::errorStr($_); ( my $to ) = $text =~ m/^To:\s*(.*?)$/im; # Lines we threw are marked, already logged, and safe to return. diff --git a/core/lib/Foswiki/Object.pm b/core/lib/Foswiki/Object.pm index cf280b85ca..c8bfbe19d1 100644 --- a/core/lib/Foswiki/Object.pm +++ b/core/lib/Foswiki/Object.pm @@ -21,11 +21,11 @@ features. =cut require Carp; -use Scalar::Util qw(blessed refaddr weaken isweak); require Foswiki::Exception; +use Try::Tiny; +use Scalar::Util qw(blessed refaddr weaken isweak); -use Moo; -use namespace::clean; +use Foswiki::Class; use Assert; @@ -84,7 +84,11 @@ has __id => ( }, ); -sub BUILDARGS { +has __clone_heap => + ( is => 'rw', clearer => 1, lazy => 1, default => sub { {} }, ); + +around BUILDARGS { + my $orig = shift; my ( $class, @params ) = @_; # Skip processing if already have passed with a hash ref. @@ -137,7 +141,7 @@ sub BUILDARGS { use strict 'refs'; return $paramHash; -} +}; sub BUILD { my $this = shift; @@ -153,7 +157,6 @@ sub BUILD { $this->__orig_line($line); $this->__orig_stack( Carp::longmess('') ); } - } sub DEMOLISH { @@ -185,9 +188,6 @@ sub DEMOLISH { } } -has __clone_heap => - ( is => 'rw', clearer => 1, lazy => 1, default => sub { {} }, ); - sub _cloneData { my $this = shift; my ( $val, $attr ) = @_; @@ -290,7 +290,7 @@ sub clone { # Debug attributes would be preserved but those coming from the # source object would be kinda pushed on a stack by adding extra __ - # prefix. This way we could trace an object history. + # prefix. This way we could trace object's history. $destAttr = "__$destAttr"; } my $clone_method = "_clone_" . $attr; diff --git a/core/lib/Foswiki/OopsException.pm b/core/lib/Foswiki/OopsException.pm index 7659ee8513..a6bb47062a 100644 --- a/core/lib/Foswiki/OopsException.pm +++ b/core/lib/Foswiki/OopsException.pm @@ -20,11 +20,11 @@ appropriate to the event that caused the exception (default 500). Extensions may throw =Foswiki::OopsException=. For example: -use Error qw(:try); +use Foswiki::Exception; ... -throw Foswiki::OopsException( 'bathplugin', +Foswiki::OopsException->throw( 'bathplugin', status => 418, web => $web, topic => $topic, diff --git a/core/lib/Foswiki/Plugin.pm b/core/lib/Foswiki/Plugin.pm index fd2a758f2b..1899fae8d9 100644 --- a/core/lib/Foswiki/Plugin.pm +++ b/core/lib/Foswiki/Plugin.pm @@ -274,9 +274,9 @@ sub registerHandlers { my $exception = ''; try { $status = $sub->( - $Foswiki::app->request->topic, - $Foswiki::app->request->web, - $users->getLoginName( $Foswiki::app->user ), + $this->app->request->topic, + $this->app->request->web, + $users->getLoginName( $this->app->user ), $this->topicWeb() ); } diff --git a/core/lib/Foswiki/Request/Attachment.pm b/core/lib/Foswiki/Request/Attachment.pm index d298715ad3..fdab3e96e6 100644 --- a/core/lib/Foswiki/Request/Attachment.pm +++ b/core/lib/Foswiki/Request/Attachment.pm @@ -18,7 +18,6 @@ package Foswiki::Request::Attachment; use v5.14; use Assert; -use Error (); use IO::File (); use Foswiki::Sandbox (); diff --git a/core/lib/Foswiki/Search.pm b/core/lib/Foswiki/Search.pm index 23828889e0..469035c059 100644 --- a/core/lib/Foswiki/Search.pm +++ b/core/lib/Foswiki/Search.pm @@ -24,10 +24,8 @@ use Foswiki::WebFilter (); use Foswiki::MetaCache (); use Foswiki::Infix::Error (); -use Moo; -use namespace::clean; +use Foswiki::Class qw(app); extends qw(Foswiki::Object); -with qw(Foswiki::AppObject); use Assert; @@ -262,11 +260,10 @@ sub searchWeb { my $this = shift; my $app = $this->app; my $req = $app->request; - ASSERT( defined $app->request->web ) if DEBUG; + ASSERT( defined $req->web ) if DEBUG; my %params = @_; - my $baseWebObject = - $this->create( 'Foswiki::Meta', web => $app->request->web ); + my $baseWebObject = $app->create( 'Foswiki::Meta', web => $req->web ); my ( $callback, $cbdata ) = setup_callback( \%params, $baseWebObject ); diff --git a/core/lib/Foswiki/Store.pm b/core/lib/Foswiki/Store.pm index 07fdf88157..1e959c6910 100644 --- a/core/lib/Foswiki/Store.pm +++ b/core/lib/Foswiki/Store.pm @@ -1,5 +1,8 @@ # See bottom of file for license and copyright information +package Foswiki::Store; +use v5.14; + =begin TML ---+ package Foswiki::Store @@ -50,9 +53,6 @@ to do any necessary encoding/decoding from/to unicode. =cut -package Foswiki::Store; -use v5.14; - use Try::Tiny; use Assert; diff --git a/core/lib/Foswiki/UI.pm b/core/lib/Foswiki/UI.pm index bea0e57c14..7f72b7396a 100644 --- a/core/lib/Foswiki/UI.pm +++ b/core/lib/Foswiki/UI.pm @@ -211,12 +211,13 @@ See Foswiki::Validation for more information. sub checkValidationKey { my $this = shift; - my $app = $this->app; - my $req = $app->request; - my $users = $app->users; + my $app = $this->app; + my $req = $app->request; + my $users = $app->users; + my $cfgData = $app->cfg->data; # If validation is disabled, do nothing - return if ( $Foswiki::cfg{Validation}{Method} eq 'none' ); + return if ( $cfgData->{Validation}{Method} eq 'none' ); # No point in command-line mode return if $app->inContext('command_line'); @@ -236,7 +237,7 @@ sub checkValidationKey { # expire the nonce - this is to support browsers that don't # implement FormData in javascript (such as IE8) Foswiki::Validation::expireValidationKeys( $users->getCGISession(), - $Foswiki::cfg{Validation}{ExpireKeyOnUse} ? $nonce : undef ); + $cfgData->{Validation}{ExpireKeyOnUse} ? $nonce : undef ); # Write a new validation code into the response my $context = $req->url( -full => 1, -path => 1, -query => 1 ) . time(); diff --git a/core/lib/Foswiki/UI/Register.pm b/core/lib/Foswiki/UI/Register.pm index bc76f91ab2..93ba42ed8a 100755 --- a/core/lib/Foswiki/UI/Register.pm +++ b/core/lib/Foswiki/UI/Register.pm @@ -20,8 +20,7 @@ use Foswiki::LoginManager (); use Foswiki::OopsException (); use Foswiki::Sandbox (); -use Moo; -use namespace::clean; +use Foswiki::Class; extends qw(Foswiki::UI); BEGIN { @@ -120,7 +119,7 @@ sub _action_register { def => 'registration_not_supported' ); } - if ( !$app->cfg->data->{Register}{EnableNewUserRegistration} ) { + if ( !$cfgData->{Register}{EnableNewUserRegistration} ) { Foswiki::OopsException->throw( app => $app, template => 'register', @@ -141,14 +140,13 @@ sub _action_register { $this->_requireConfirmation( $data, 'Verification', 'confirm', $data->{Email} ); } - elsif ( $Foswiki::cfg{Register}{NeedApproval} ) { + elsif ( $cfgData->{Register}{NeedApproval} ) { my $query = $app->request; my $data = _getDataFromQuery( $app->users, $query ); $data->{FirstLastName} = $data->{Name}; - my $approvers = $Foswiki::cfg{Register}{Approvers} - || $Foswiki::cfg{AdminUserWikiName}; - _requireConfirmation( $app, $data, 'Approval', 'approve', - $approvers ); + my $approvers = $cfgData->{Register}{Approvers} + || $cfgData->{AdminUserWikiName}; + _requireConfirmation( $app, $data, 'Approval', 'approve', $approvers ); } else { @@ -954,7 +952,7 @@ sub deleteUser { # This is the old behavior - remove the current logged in user. For safety # Make sure the requested user = current user. - unless ( Foswiki::Func::isAnAdmin() ) { + unless ( $app->isAnAdmin() ) { if ( ( $user ne $cUID ) && ( $myWikiName ne $userWikiName ) ) { Foswiki::OopsException->throw( @@ -1014,7 +1012,7 @@ sub deleteUser { { cuid => $user, removeTopic => $removeTopic, prefix => $topicPrefix } ); - Foswiki::Func::writeWarning("$cUID: $lm"); + $app->writeWarning("$cUID: $lm"); Foswiki::OopsException->throw( app => $app, @@ -1077,7 +1075,7 @@ sub addUserToGroup { # list, and the group exists # then we're trying to upgrade the user topic. # I'm not sure what other mappers might make of this.. - if ( $create and Foswiki::Func::isGroup($groupName) ) { + if ( $create and $app->users->isGroup($groupName) ) { try { $users->addUserToGroup( undef, $groupName, $create ); } @@ -1100,7 +1098,7 @@ sub addUserToGroup { } } - if ( !Foswiki::Func::isGroup($groupName) + if ( !$app->users->isGroup($groupName) && !$create ) { Foswiki::OopsException->throw( @@ -1123,7 +1121,7 @@ sub addUserToGroup { # He can afterwards remove himself if needed # We make an exception if you are an admin as they can always edit anything - if ( !Foswiki::Func::isGroup($groupName) + if ( !$app->users->isGroup($groupName) and !$users->isAdmin($user) and $create ) { @@ -1144,9 +1142,8 @@ sub addUserToGroup { $u = '' if ( $u eq '' ); next - if ( Foswiki::Func::isGroup($groupName) - && Foswiki::Func::isGroupMember( $groupName, $u, { expand => 0 } ) - ); + if ( $app->users->isGroup($groupName) + && $app->isGroupMember( $groupName, $u, { expand => 0 } ) ); try { $u = $users->validateRegistrationField( 'username', $u ); @@ -1237,7 +1234,7 @@ sub removeUserFromGroup { def => 'no_group_specified_for_remove_from_group' ); } - unless ( Foswiki::Func::isGroup($groupName) ) { + unless ( $app->users->isGroup($groupName) ) { Foswiki::OopsException->throw( app => $app, template => 'register', @@ -1256,7 +1253,7 @@ sub removeUserFromGroup { next if ( $u eq '' ); try { - Foswiki::Func::removeUserFromGroup( $u, $groupName ); + $app->removeUserFromGroup( $u, $groupName ); push( @succeeded, $u ); } catch { @@ -1362,7 +1359,7 @@ sub _complete { my $regoAgent = $app->user; my $enableAddToGroup = 1; - if ( Foswiki::Func::isGuest($regoAgent) ) { + if ( $app->users->isGuest( $app->getCanonicalUserID($regoAgent) ) ) { $app->user( $app->users->getCanonicalUserID( $cfgData->{Register}{RegistrationAgentWikiName} @@ -1399,16 +1396,12 @@ sub _complete { push @addedTo, $groupName; } catch { - my $e = shift; $app->logger->log( 'warning', "Registration: Failure adding $cUID to $groupName" ); + Foswiki::Exception::Fatal->rethrow($_); } finally { $app->user($safe); - if (@_) { - $_[0]->throw; - } - }; } } @@ -1416,35 +1409,27 @@ sub _complete { $data->{AddToGroups} = join( ',', @addedTo ); } catch { - my $e = $_; + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + + # Whatever went wrong – remove the incompletely registered user. + $users->removeUser( $data->{LoginName}, $data->{WikiName} ) + if ( $users->userExists( $data->{WikiName} ) ); + if ( $e->isa('Foswiki::OopsException') ) { - $users->removeUser( $data->{LoginName}, $data->{WikiName} ) - if ( $users->userExists( $data->{WikiName} ) ); $e->rethrow; - - #Foswiki::OopsException->throw ( @_ ); # Propagate } - else { - - if ( !ref($e) ) { - Foswiki::Exception->rethrow($e); - } - - $users->removeUser( $data->{LoginName}, $data->{WikiName} ) - if ( $users->userExists( $data->{WikiName} ) ); - # Log the error - $app->logger->log( 'warning', - 'Registration failed: ' . $e->stringify() ); - Foswiki::OopsException->throw( - app => $app, - template => 'register', - web => $data->{webName}, - topic => $topic, - def => 'problem_adding', - params => [ $data->{WikiName}, $e->stringify() ] - ); - } + # Log the error + my $errStr = Foswiki::Exception::errorStr($e); + $app->logger->log( 'warning', 'Registration failed: ' . $errStr ); + Foswiki::OopsException->throw( + app => $app, + template => 'register', + web => $data->{webName}, + topic => $topic, + def => 'problem_adding', + params => [ $data->{WikiName}, $errStr ] + ); }; # Plugin to do some other post processing of the user. @@ -1546,7 +1531,7 @@ sub _createUserTopic { ($template) = $template =~ m/^(.*)$/; ( $fromWeb, $template ) = - Foswiki::Func::normalizeWebTopicName( $fromWeb, $template ); + $app->request->normalizeWebTopicName( $fromWeb, $template ); if ( !$app->store->topicExists( $fromWeb, $template ) ) { @@ -2054,7 +2039,7 @@ sub _validateRegistration { # Optional check if email address is already registered if ( $cfgData->{Register}{UniqueEmail} ) { - my @existingNames = Foswiki::Func::emailToWikiNames( $data->{Email} ); + my @existingNames = $app->emailToWikiNames( $data->{Email} ); if ( $cfgData->{Register}{NeedVerification} ) { my @pending = $this->_checkPendingRegistrations( $data->{Email}, $exp ); @@ -2151,7 +2136,7 @@ sub _validateTemplateTopic { my $req = $app->request; my ( $templateWeb, $templateTopic ) = - Foswiki::Func::normalizeWebTopicName( $cfgData->{UserWebName}, + $req->normalizeWebTopicName( $cfgData->{UserWebName}, $_[0] || 'NewUserTemplate' ); $templateTopic = Foswiki::Sandbox::untaint( @@ -2497,15 +2482,16 @@ sub _processDeleteUser { my $this = shift; my $paramHash = shift; - my $cfgData = $this->app->cfg->data; + my $app = $this->app; + my $cfgData = $app->cfg->data; my $user = $paramHash->{cuid}; # Obtain all the user info before removing things. If there is no mapping # for the user, then assume the entered username will be removed. - my $cUID = Foswiki::Func::getCanonicalUserID($user); - my $wikiname = ($cUID) ? Foswiki::Func::getWikiName($cUID) : $user; - my $email = join( ',', Foswiki::Func::wikinameToEmails($wikiname) ); + my $cUID = $app->getCanonicalUserID($user); + my $wikiname = ($cUID) ? $app->getWikiName($cUID) : $user; + my $email = join( ',', $app->wikinameToEmails($wikiname) ); my ( $message, $logMessage ) = ( "Processing $wikiname($email)\n", "Processing $wikiname($email) " ); @@ -2536,25 +2522,23 @@ sub _processDeleteUser { } # If a group topic has been entered, don't remove it. - if ( Foswiki::Func::isGroup($wikiname) ) { + if ( $app->users->isGroup($wikiname) ) { $message .= " Cannot remove group $wikiname \n"; $logMessage .= "Cannot remove group $wikiname, "; return ( $message, $logMessage ); } # Remove the user from any groups. - my $it = Foswiki::Func::eachGroup(); + my $it = $app->users->eachGroup(); $logMessage .= "Removed from groups: "; while ( $it->hasNext() ) { my $group = $it->next(); #$message .= "Checking $group for ($wikiname)\n"; - if ( - Foswiki::Func::isGroupMember( $group, $wikiname, { expand => 0 } ) ) - { + if ( $app->isGroupMember( $group, $wikiname, { expand => 0 } ) ) { $message .= "user removed from $group \n"; $logMessage .= "$group, "; - Foswiki::Func::removeUserFromGroup( $wikiname, $group ); + $app->removeUserFromGroup( $wikiname, $group ); } } @@ -2562,9 +2546,9 @@ sub _processDeleteUser { # Remove the users topic, moving it to trash web ( my $web, $wikiname ) = - Foswiki::Func::normalizeWebTopicName( $cfgData->{UsersWebName}, + $app->request->normalizeWebTopicName( $cfgData->{UsersWebName}, $wikiname ); - if ( Foswiki::Func::topicExists( $web, $wikiname ) ) { + if ( $app->topicExists( $web, $wikiname ) ) { # Spoof the user so we can delete their topic. Don't need to # do this for the REST handler, but we do for the registration @@ -2573,7 +2557,7 @@ sub _processDeleteUser { my $newTopic = "$paramHash->{prefix}$wikiname" . time; try { - Foswiki::Func::moveTopic( $web, $wikiname, + $app->moveTopic( $web, $wikiname, $cfgData->{TrashWebName}, $newTopic ); $message .= " - user topic moved to $cfgData->{TrashWebName}.$newTopic \n"; diff --git a/core/lib/Foswiki/Users.pm b/core/lib/Foswiki/Users.pm index 570e844eaa..eb9424748b 100644 --- a/core/lib/Foswiki/Users.pm +++ b/core/lib/Foswiki/Users.pm @@ -1,5 +1,8 @@ # See bottom of file for license and copyright information +package Foswiki::Users; +use v5.14; + =begin TML ---+ package Foswiki::Users @@ -54,9 +57,6 @@ to a user. =cut -package Foswiki::Users; -use v5.14; - use Foswiki::AggregateIterator (); use Foswiki::LoginManager (); @@ -278,7 +278,8 @@ sub initialiseUser { # plugins can provide an alternate login name. #my $plogin = $this->app->plugins->load(); - my $plogin = $this->app->engine->user; + my $plogin = $this->app->engine->user; + my $cfgData = $this->app->cfg->data; #Monitor::MARK("Plugins loaded"); @@ -308,7 +309,7 @@ sub initialiseUser { $this->cUID2WikiName->{$cUID} = $login; # needs to be WikiName safe - $this->cUID2WikiName->{$cUID} =~ s/$Foswiki::cfg{NameFilter}//g; + $this->cUID2WikiName->{$cUID} =~ s/$cfgData->{NameFilter}//g; $this->cUID2WikiName->{$cUID} =~ s/\.//g; $this->login2cUID->{$login} = $cUID; @@ -318,7 +319,7 @@ sub initialiseUser { # if we get here without a login id, we are a guest. Get the guest # cUID. - $cUID ||= $this->getCanonicalUserID( $Foswiki::cfg{DefaultUserLogin} ); + $cUID ||= $this->getCanonicalUserID( $cfgData->{DefaultUserLogin} ); return $cUID; } @@ -623,6 +624,9 @@ True if the user is an admin sub isAdmin { my ( $this, $cUID ) = @_; + # SMELL Why do we demain exclusively cUID here? What's the problem to + # call getCanonicalUserID()? + return 0 unless defined $cUID; return $this->_isAdmin->{$cUID} @@ -644,6 +648,22 @@ sub isAdmin { =begin TML +---++ ObjectMethod isGuest( $cUID ) -> $boolean + +True if the =$cUID= is guest. + +=cut + +sub isGuest { + my $this = shift; + my ($cUID) = @_; + + return $cUID eq + $this->getCanonicalUserID( $this->app->cfg->data->{DefaultUserLogin} ); +} + +=begin TML + ---++ ObjectMethod isInUserList( $cUID, \@list ) -> $boolean Return true if $cUID is in a list of user *wikinames*, *logins* and group ids.