diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/shell')
4 files changed, 421 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php b/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php new file mode 100644 index 00000000..b031431a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php @@ -0,0 +1,50 @@ +<?php + +use MediaWiki\Shell\Command; +use MediaWiki\Shell\CommandFactory; +use MediaWiki\Shell\FirejailCommand; +use Psr\Log\NullLogger; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Shell + */ +class CommandFactoryTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + /** + * @covers MediaWiki\Shell\CommandFactory::create + */ + public function testCreate() { + $logger = new NullLogger(); + $cgroup = '/sys/fs/cgroup/memory/mygroup'; + $limits = [ + 'filesize' => 1000, + 'memory' => 1000, + 'time' => 30, + 'walltime' => 40, + ]; + + $factory = new CommandFactory( $limits, $cgroup, false ); + $factory->setLogger( $logger ); + $factory->logStderr(); + $command = $factory->create(); + $this->assertInstanceOf( Command::class, $command ); + + $wrapper = TestingAccessWrapper::newFromObject( $command ); + $this->assertSame( $logger, $wrapper->logger ); + $this->assertSame( $cgroup, $wrapper->cgroup ); + $this->assertSame( $limits, $wrapper->limits ); + $this->assertTrue( $wrapper->doLogStderr ); + } + + /** + * @covers MediaWiki\Shell\CommandFactory::create + */ + public function testFirejailCreate() { + $factory = new CommandFactory( [], false, 'firejail' ); + $factory->setLogger( new NullLogger() ); + $this->assertInstanceOf( FirejailCommand::class, $factory->create() ); + } +} diff --git a/www/wiki/tests/phpunit/includes/shell/CommandTest.php b/www/wiki/tests/phpunit/includes/shell/CommandTest.php new file mode 100644 index 00000000..2e031638 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/shell/CommandTest.php @@ -0,0 +1,181 @@ +<?php + +use MediaWiki\Shell\Command; +use Wikimedia\TestingAccessWrapper; + +/** + * @covers \MediaWiki\Shell\Command + * @group Shell + */ +class CommandTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + private function requirePosix() { + if ( wfIsWindows() ) { + $this->markTestSkipped( 'This test requires a POSIX environment.' ); + } + } + + /** + * @dataProvider provideExecute + */ + public function testExecute( $commandInput, $expectedExitCode, $expectedOutput ) { + $this->requirePosix(); + + $command = new Command(); + $result = $command + ->params( $commandInput ) + ->execute(); + + $this->assertSame( $expectedExitCode, $result->getExitCode() ); + $this->assertSame( $expectedOutput, $result->getStdout() ); + } + + public function provideExecute() { + return [ + 'success status' => [ 'true', 0, '' ], + 'failure status' => [ 'false', 1, '' ], + 'output' => [ [ 'echo', '-n', 'x', '>', 'y' ], 0, 'x > y' ], + ]; + } + + public function testEnvironment() { + $this->requirePosix(); + + $command = new Command(); + $result = $command + ->params( [ 'printenv', 'FOO' ] ) + ->environment( [ 'FOO' => 'bar' ] ) + ->execute(); + $this->assertSame( "bar\n", $result->getStdout() ); + } + + public function testStdout() { + $this->requirePosix(); + + $command = new Command(); + + $result = $command + ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ) + ->execute(); + + $this->assertNotContains( 'ThisIsStderr', $result->getStdout() ); + $this->assertEquals( "ThisIsStderr\n", $result->getStderr() ); + } + + public function testStdoutRedirection() { + $this->requirePosix(); + + $command = new Command(); + + $result = $command + ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ) + ->includeStderr( true ) + ->execute(); + + $this->assertEquals( "ThisIsStderr\n", $result->getStdout() ); + $this->assertNull( $result->getStderr() ); + } + + public function testOutput() { + global $IP; + + $this->requirePosix(); + chdir( $IP ); + + $command = new Command(); + $result = $command + ->params( [ 'ls', 'index.php' ] ) + ->execute(); + $this->assertRegExp( '/^index.php$/m', $result->getStdout() ); + $this->assertSame( null, $result->getStderr() ); + + $command = new Command(); + $result = $command + ->params( [ 'ls', 'index.php', 'no-such-file' ] ) + ->includeStderr() + ->execute(); + $this->assertRegExp( '/^index.php$/m', $result->getStdout() ); + $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStdout() ); + $this->assertSame( null, $result->getStderr() ); + + $command = new Command(); + $result = $command + ->params( [ 'ls', 'index.php', 'no-such-file' ] ) + ->execute(); + $this->assertRegExp( '/^index.php$/m', $result->getStdout() ); + $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStderr() ); + } + + /** + * Test that null values are skipped by params() and unsafeParams() + */ + public function testNullsAreSkipped() { + $command = TestingAccessWrapper::newFromObject( new Command ); + $command->params( 'echo', 'a', null, 'b' ); + $command->unsafeParams( 'c', null, 'd' ); + $this->assertEquals( "'echo' 'a' 'b' c d", $command->command ); + } + + public function testT69870() { + $commandLine = wfIsWindows() + // 333 = 331 + CRLF + ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) ) + : 'printf "%-333333s" "*"'; + + // Test several times because it involves a race condition that may randomly succeed or fail + for ( $i = 0; $i < 10; $i++ ) { + $command = new Command(); + $output = $command->unsafeParams( $commandLine ) + ->execute() + ->getStdout(); + $this->assertEquals( 333333, strlen( $output ) ); + } + } + + public function testLogStderr() { + $this->requirePosix(); + + $logger = new TestLogger( true, function ( $message, $level, $context ) { + return $level === Psr\Log\LogLevel::ERROR ? '1' : null; + }, true ); + $command = new Command(); + $command->setLogger( $logger ); + $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ); + $command->execute(); + $this->assertEmpty( $logger->getBuffer() ); + + $command = new Command(); + $command->setLogger( $logger ); + $command->logStderr(); + $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ); + $command->execute(); + $this->assertSame( 1, count( $logger->getBuffer() ) ); + $this->assertSame( trim( $logger->getBuffer()[0][2]['error'] ), 'ThisIsStderr' ); + } + + public function testInput() { + $this->requirePosix(); + + $command = new Command(); + $command->params( 'cat' ); + $command->input( 'abc' ); + $result = $command->execute(); + $this->assertSame( 'abc', $result->getStdout() ); + + // now try it with something that does not fit into a single block + $command = new Command(); + $command->params( 'cat' ); + $command->input( str_repeat( '!', 1000000 ) ); + $result = $command->execute(); + $this->assertSame( 1000000, strlen( $result->getStdout() ) ); + + // And try it with empty input + $command = new Command(); + $command->params( 'cat' ); + $command->input( '' ); + $result = $command->execute(); + $this->assertSame( '', $result->getStdout() ); + } +} diff --git a/www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php b/www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php new file mode 100644 index 00000000..681c3dcd --- /dev/null +++ b/www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +use MediaWiki\Shell\FirejailCommand; +use MediaWiki\Shell\Shell; +use Wikimedia\TestingAccessWrapper; + +class FirejailCommandTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function provideBuildFinalCommand() { + global $IP; + // phpcs:ignore Generic.Files.LineLength + $env = "'MW_INCLUDE_STDERR=;MW_CPU_LIMIT=180; MW_CGROUP='\'''\''; MW_MEM_LIMIT=307200; MW_FILE_SIZE_LIMIT=102400; MW_WALL_CLOCK_LIMIT=180; MW_USE_LOG_PIPE=yes'"; + $limit = "/bin/bash '$IP/includes/shell/limit.sh'"; + $profile = "--profile=$IP/includes/shell/firejail.profile"; + $blacklist = '--blacklist=' . realpath( MW_CONFIG_FILE ); + $default = "$blacklist --noroot --seccomp --private-dev"; + return [ + [ + 'No restrictions', + 'ls', 0, "$limit ''\''ls'\''' $env" + ], + [ + 'default restriction', + 'ls', Shell::RESTRICT_DEFAULT, + "$limit 'firejail --quiet $profile $default -- '\''ls'\''' $env" + ], + [ + 'no network', + 'ls', Shell::NO_NETWORK, + "$limit 'firejail --quiet $profile --net=none -- '\''ls'\''' $env" + ], + [ + 'default restriction & no network', + 'ls', Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK, + "$limit 'firejail --quiet $profile $default --net=none -- '\''ls'\''' $env" + ], + [ + 'seccomp', + 'ls', Shell::SECCOMP, + "$limit 'firejail --quiet $profile --seccomp -- '\''ls'\''' $env" + ], + [ + 'seccomp & no execve', + 'ls', Shell::SECCOMP | Shell::NO_EXECVE, + "$limit 'firejail --quiet $profile --shell=none --seccomp=execve -- '\''ls'\''' $env" + ], + ]; + } + + /** + * @covers \MediaWiki\Shell\FirejailCommand::buildFinalCommand() + * @dataProvider provideBuildFinalCommand + */ + public function testBuildFinalCommand( $desc, $params, $flags, $expected ) { + $command = new FirejailCommand( 'firejail' ); + $command + ->params( $params ) + ->restrict( $flags ); + $wrapper = TestingAccessWrapper::newFromObject( $command ); + $output = $wrapper->buildFinalCommand( $wrapper->command ); + $this->assertEquals( $expected, $output[0], $desc ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/shell/ShellTest.php b/www/wiki/tests/phpunit/includes/shell/ShellTest.php new file mode 100644 index 00000000..bf46f44b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/shell/ShellTest.php @@ -0,0 +1,105 @@ +<?php + +use MediaWiki\Shell\Command; +use MediaWiki\Shell\Shell; +use Wikimedia\TestingAccessWrapper; + +/** + * @covers \MediaWiki\Shell\Shell + * @group Shell + */ +class ShellTest extends MediaWikiTestCase { + + use MediaWikiCoversValidator; + + public function testIsDisabled() { + $this->assertInternalType( 'bool', Shell::isDisabled() ); // sanity + } + + /** + * @dataProvider provideEscape + */ + public function testEscape( $args, $expected ) { + if ( wfIsWindows() ) { + $this->markTestSkipped( 'This test requires a POSIX environment.' ); + } + $this->assertSame( $expected, call_user_func_array( [ Shell::class, 'escape' ], $args ) ); + } + + public function provideEscape() { + return [ + 'simple' => [ [ 'true' ], "'true'" ], + 'with args' => [ [ 'convert', '-font', 'font name' ], "'convert' '-font' 'font name'" ], + 'array' => [ [ [ 'convert', '-font', 'font name' ] ], "'convert' '-font' 'font name'" ], + 'skip nulls' => [ [ 'ls', null ], "'ls'" ], + ]; + } + + /** + * @covers \MediaWiki\Shell\Shell::makeScriptCommand + * @dataProvider provideMakeScriptCommand + * + * @param string $expected + * @param string $script + * @param string[] $parameters + * @param string[] $options + * @param callable|null $hook + */ + public function testMakeScriptCommand( $expected, + $script, + $parameters, + $options = [], + $hook = null + ) { + // Running tests under Vagrant involves MWMultiVersion that uses the below hook + $this->setMwGlobals( 'wgHooks', [] ); + + if ( $hook ) { + $this->setTemporaryHook( 'wfShellWikiCmd', $hook ); + } + + $command = Shell::makeScriptCommand( $script, $parameters, $options ); + $command->params( 'safe' ) + ->unsafeParams( 'unsafe' ); + + $this->assertType( Command::class, $command ); + + $wrapper = TestingAccessWrapper::newFromObject( $command ); + $this->assertEquals( $expected, $wrapper->command ); + $this->assertEquals( 0, $wrapper->restrictions & Shell::NO_LOCALSETTINGS ); + } + + public function provideMakeScriptCommand() { + global $wgPhpCli; + + return [ + [ + "'$wgPhpCli' 'maintenance/foobar.php' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + ], + [ + "'$wgPhpCli' 'changed.php' '--wiki=somewiki' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + [], + function ( &$script, array &$parameters ) { + $script = 'changed.php'; + array_unshift( $parameters, '--wiki=somewiki' ); + } + ], + [ + "'/bin/perl' 'maintenance/foobar.php' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + [ 'php' => '/bin/perl' ], + ], + [ + "'$wgPhpCli' 'foobinize' 'maintenance/foobar.php' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + [ 'wrapper' => 'foobinize' ], + ], + ]; + } +} |