diff options
Diffstat (limited to 'tests/lib/AppFramework')
63 files changed, 13033 insertions, 0 deletions
diff --git a/tests/lib/AppFramework/AppTest.php b/tests/lib/AppFramework/AppTest.php new file mode 100644 index 00000000000..f9b7cf50675 --- /dev/null +++ b/tests/lib/AppFramework/AppTest.php @@ -0,0 +1,220 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework; + +use OC\AppFramework\App; +use OC\AppFramework\DependencyInjection\DIContainer; +use OC\AppFramework\Http\Dispatcher; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\IOutput; +use OCP\AppFramework\Http\Response; + +function rrmdir($directory) { + $files = array_diff(scandir($directory), ['.','..']); + foreach ($files as $file) { + if (is_dir($directory . '/' . $file)) { + rrmdir($directory . '/' . $file); + } else { + unlink($directory . '/' . $file); + } + } + return rmdir($directory); +} + + +class AppTest extends \Test\TestCase { + private DIContainer $container; + private $io; + private $api; + private $controller; + private $dispatcher; + private $params; + private $headers; + private $output; + private $controllerName; + private $controllerMethod; + private $appPath; + + protected function setUp(): void { + parent::setUp(); + + $this->container = new DIContainer('test', []); + $this->controller = $this->createMock(Controller::class); + $this->dispatcher = $this->createMock(Dispatcher::class); + $this->io = $this->createMock(IOutput::class); + + $this->headers = ['key' => 'value']; + $this->output = 'hi'; + $this->controllerName = 'Controller'; + $this->controllerMethod = 'method'; + + $this->container[$this->controllerName] = $this->controller; + $this->container[Dispatcher::class] = $this->dispatcher; + $this->container[IOutput::class] = $this->io; + $this->container['urlParams'] = ['_route' => 'not-profiler']; + + $this->appPath = __DIR__ . '/../../../apps/namespacetestapp'; + $infoXmlPath = $this->appPath . '/appinfo/info.xml'; + mkdir($this->appPath . '/appinfo', 0777, true); + + $xml = '<?xml version="1.0" encoding="UTF-8"?>' + . '<info>' + . '<id>namespacetestapp</id>' + . '<namespace>NameSpaceTestApp</namespace>' + . '</info>'; + file_put_contents($infoXmlPath, $xml); + } + + + public function testControllerNameAndMethodAreBeingPassed(): void { + $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + + $this->io->expects($this->never()) + ->method('setOutput'); + + App::main($this->controllerName, $this->controllerMethod, + $this->container); + } + + + public function testBuildAppNamespace(): void { + $ns = App::buildAppNamespace('someapp'); + $this->assertEquals('OCA\Someapp', $ns); + } + + + public function testBuildAppNamespaceCore(): void { + $ns = App::buildAppNamespace('someapp', 'OC\\'); + $this->assertEquals('OC\Someapp', $ns); + } + + + public function testBuildAppNamespaceInfoXml(): void { + $ns = App::buildAppNamespace('namespacetestapp', 'OCA\\'); + $this->assertEquals('OCA\NameSpaceTestApp', $ns); + } + + + protected function tearDown(): void { + rrmdir($this->appPath); + parent::tearDown(); + } + + + public function testOutputIsPrinted(): void { + $return = ['HTTP/2.0 200 OK', [], [], $this->output, new Response()]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + $this->io->expects($this->once()) + ->method('setOutput') + ->with($this->equalTo($this->output)); + App::main($this->controllerName, $this->controllerMethod, $this->container, []); + } + + public static function dataNoOutput(): array { + return [ + ['HTTP/2.0 204 No content'], + ['HTTP/2.0 304 Not modified'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOutput')] + public function testNoOutput(string $statusCode): void { + $return = [$statusCode, [], [], $this->output, new Response()]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + $this->io->expects($this->once()) + ->method('setHeader') + ->with($this->equalTo($statusCode)); + $this->io->expects($this->never()) + ->method('setOutput'); + App::main($this->controllerName, $this->controllerMethod, $this->container, []); + } + + + public function testCallbackIsCalled(): void { + $mock = $this->getMockBuilder('OCP\AppFramework\Http\ICallbackResponse') + ->getMock(); + + $return = ['HTTP/2.0 200 OK', [], [], $this->output, $mock]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + $mock->expects($this->once()) + ->method('callback'); + App::main($this->controllerName, $this->controllerMethod, $this->container, []); + } + + public function testCoreApp(): void { + $this->container['appName'] = 'core'; + $this->container['OC\Core\Controller\Foo'] = $this->controller; + $this->container['urlParams'] = ['_route' => 'not-profiler']; + + $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + + $this->io->expects($this->never()) + ->method('setOutput'); + + App::main('Foo', $this->controllerMethod, $this->container); + } + + public function testSettingsApp(): void { + $this->container['appName'] = 'settings'; + $this->container['OCA\Settings\Controller\Foo'] = $this->controller; + $this->container['urlParams'] = ['_route' => 'not-profiler']; + + $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + + $this->io->expects($this->never()) + ->method('setOutput'); + + App::main('Foo', $this->controllerMethod, $this->container); + } + + public function testApp(): void { + $this->container['appName'] = 'bar'; + $this->container['OCA\Bar\Controller\Foo'] = $this->controller; + $this->container['urlParams'] = ['_route' => 'not-profiler']; + + $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willReturn($return); + + $this->io->expects($this->never()) + ->method('setOutput'); + + App::main('Foo', $this->controllerMethod, $this->container); + } +} diff --git a/tests/lib/AppFramework/Bootstrap/BootContextTest.php b/tests/lib/AppFramework/Bootstrap/BootContextTest.php new file mode 100644 index 00000000000..3a97ff2bbfc --- /dev/null +++ b/tests/lib/AppFramework/Bootstrap/BootContextTest.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\AppFramework\Bootstrap; + +use OC\AppFramework\Bootstrap\BootContext; +use OCP\AppFramework\IAppContainer; +use OCP\IServerContainer; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class BootContextTest extends TestCase { + /** @var IAppContainer|MockObject */ + private $appContainer; + + /** @var BootContext */ + private $context; + + protected function setUp(): void { + parent::setUp(); + + $this->appContainer = $this->createMock(IAppContainer::class); + + $this->context = new BootContext( + $this->appContainer + ); + } + + public function testGetAppContainer(): void { + $container = $this->context->getAppContainer(); + + $this->assertSame($this->appContainer, $container); + } + + public function testGetServerContainer(): void { + $serverContainer = $this->createMock(IServerContainer::class); + $this->appContainer->method('get') + ->with(IServerContainer::class) + ->willReturn($serverContainer); + + $container = $this->context->getServerContainer(); + + $this->assertSame($serverContainer, $container); + } +} diff --git a/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php b/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php new file mode 100644 index 00000000000..0eeddb2173a --- /dev/null +++ b/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\AppFramework\Bootstrap; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Support\CrashReport\Registry; +use OCA\Settings\AppInfo\Application; +use OCP\App\IAppManager; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\QueryException; +use OCP\Dashboard\IManager; +use OCP\Diagnostics\IEventLogger; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IServerContainer; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CoordinatorTest extends TestCase { + /** @var IAppManager|MockObject */ + private $appManager; + + /** @var IServerContainer|MockObject */ + private $serverContainer; + + /** @var Registry|MockObject */ + private $crashReporterRegistry; + + /** @var IManager|MockObject */ + private $dashboardManager; + + /** @var IEventDispatcher|MockObject */ + private $eventDispatcher; + + /** @var IEventLogger|MockObject */ + private $eventLogger; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var Coordinator */ + private $coordinator; + + protected function setUp(): void { + parent::setUp(); + + $this->appManager = $this->createMock(IAppManager::class); + $this->serverContainer = $this->createMock(IServerContainer::class); + $this->crashReporterRegistry = $this->createMock(Registry::class); + $this->dashboardManager = $this->createMock(IManager::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->eventLogger = $this->createMock(IEventLogger::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->coordinator = new Coordinator( + $this->serverContainer, + $this->crashReporterRegistry, + $this->dashboardManager, + $this->eventDispatcher, + $this->eventLogger, + $this->appManager, + $this->logger, + ); + } + + public function testBootAppNotLoadable(): void { + $appId = 'settings'; + $this->serverContainer->expects($this->once()) + ->method('query') + ->with(Application::class) + ->willThrowException(new QueryException('')); + $this->logger->expects($this->once()) + ->method('error'); + + $this->coordinator->bootApp($appId); + } + + public function testBootAppNotBootable(): void { + $appId = 'settings'; + $mockApp = $this->createMock(Application::class); + $this->serverContainer->expects($this->once()) + ->method('query') + ->with(Application::class) + ->willReturn($mockApp); + + $this->coordinator->bootApp($appId); + } + + public function testBootApp(): void { + $appId = 'settings'; + $mockApp = new class extends App implements IBootstrap { + public function __construct() { + parent::__construct('test', []); + } + + public function register(IRegistrationContext $context): void { + } + + public function boot(IBootContext $context): void { + } + }; + $this->serverContainer->expects($this->once()) + ->method('query') + ->with(Application::class) + ->willReturn($mockApp); + + $this->coordinator->bootApp($appId); + } +} diff --git a/tests/lib/AppFramework/Bootstrap/FunctionInjectorTest.php b/tests/lib/AppFramework/Bootstrap/FunctionInjectorTest.php new file mode 100644 index 00000000000..8f6944ce34f --- /dev/null +++ b/tests/lib/AppFramework/Bootstrap/FunctionInjectorTest.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\AppFramework\Bootstrap; + +use OC\AppFramework\Bootstrap\FunctionInjector; +use OC\AppFramework\Utility\SimpleContainer; +use OCP\AppFramework\QueryException; +use Test\TestCase; + +interface Foo { +} + +class FunctionInjectorTest extends TestCase { + /** @var SimpleContainer */ + private $container; + + protected function setUp(): void { + parent::setUp(); + + $this->container = new SimpleContainer(); + } + + public function testInjectFnNotRegistered(): void { + $this->expectException(QueryException::class); + + (new FunctionInjector($this->container))->injectFn(static function (Foo $p1): void { + }); + } + + public function testInjectFnNotRegisteredButNullable(): void { + (new FunctionInjector($this->container))->injectFn(static function (?Foo $p1): void { + }); + + // Nothing to assert. No errors means everything is fine. + $this->addToAssertionCount(1); + } + + public function testInjectFnByType(): void { + $this->container->registerService(Foo::class, function () { + $this->addToAssertionCount(1); + return new class implements Foo { + }; + }); + + (new FunctionInjector($this->container))->injectFn(static function (Foo $p1): void { + }); + + // Nothing to assert. No errors means everything is fine. + $this->addToAssertionCount(1); + } + + public function testInjectFnByName(): void { + $this->container->registerParameter('test', 'abc'); + + (new FunctionInjector($this->container))->injectFn(static function ($test): void { + }); + + // Nothing to assert. No errors means everything is fine. + $this->addToAssertionCount(1); + } +} diff --git a/tests/lib/AppFramework/Bootstrap/RegistrationContextTest.php b/tests/lib/AppFramework/Bootstrap/RegistrationContextTest.php new file mode 100644 index 00000000000..c0095713370 --- /dev/null +++ b/tests/lib/AppFramework/Bootstrap/RegistrationContextTest.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\AppFramework\Bootstrap; + +use OC\AppFramework\Bootstrap\RegistrationContext; +use OC\AppFramework\Bootstrap\ServiceRegistration; +use OC\Core\Middleware\TwoFactorMiddleware; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; +use OCP\EventDispatcher\IEventDispatcher; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class RegistrationContextTest extends TestCase { + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var RegistrationContext */ + private $context; + + protected function setUp(): void { + parent::setUp(); + + $this->logger = $this->createMock(LoggerInterface::class); + + $this->context = new RegistrationContext( + $this->logger + ); + } + + public function testRegisterCapability(): void { + $app = $this->createMock(App::class); + $name = 'abc'; + $container = $this->createMock(IAppContainer::class); + $app->method('getContainer') + ->willReturn($container); + $container->expects($this->once()) + ->method('registerCapability') + ->with($name); + $this->logger->expects($this->never()) + ->method('error'); + + $this->context->for('myapp')->registerCapability($name); + $this->context->delegateCapabilityRegistrations([ + 'myapp' => $app, + ]); + } + + public function testRegisterEventListener(): void { + $event = 'abc'; + $service = 'def'; + $dispatcher = $this->createMock(IEventDispatcher::class); + $dispatcher->expects($this->once()) + ->method('addServiceListener') + ->with($event, $service, 0); + $this->logger->expects($this->never()) + ->method('error'); + + $this->context->for('myapp')->registerEventListener($event, $service); + $this->context->delegateEventListenerRegistrations($dispatcher); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider_TrueFalse')] + public function testRegisterService(bool $shared): void { + $app = $this->createMock(App::class); + $service = 'abc'; + $factory = function () { + return 'def'; + }; + $container = $this->createMock(IAppContainer::class); + $app->method('getContainer') + ->willReturn($container); + $container->expects($this->once()) + ->method('registerService') + ->with($service, $factory, $shared); + $this->logger->expects($this->never()) + ->method('error'); + + $this->context->for('myapp')->registerService($service, $factory, $shared); + $this->context->delegateContainerRegistrations([ + 'myapp' => $app, + ]); + } + + public function testRegisterServiceAlias(): void { + $app = $this->createMock(App::class); + $alias = 'abc'; + $target = 'def'; + $container = $this->createMock(IAppContainer::class); + $app->method('getContainer') + ->willReturn($container); + $container->expects($this->once()) + ->method('registerAlias') + ->with($alias, $target); + $this->logger->expects($this->never()) + ->method('error'); + + $this->context->for('myapp')->registerServiceAlias($alias, $target); + $this->context->delegateContainerRegistrations([ + 'myapp' => $app, + ]); + } + + public function testRegisterParameter(): void { + $app = $this->createMock(App::class); + $name = 'abc'; + $value = 'def'; + $container = $this->createMock(IAppContainer::class); + $app->method('getContainer') + ->willReturn($container); + $container->expects($this->once()) + ->method('registerParameter') + ->with($name, $value); + $this->logger->expects($this->never()) + ->method('error'); + + $this->context->for('myapp')->registerParameter($name, $value); + $this->context->delegateContainerRegistrations([ + 'myapp' => $app, + ]); + } + + public function testRegisterUserMigrator(): void { + $appIdA = 'myapp'; + $migratorClassA = 'OCA\App\UserMigration\AppMigrator'; + + $appIdB = 'otherapp'; + $migratorClassB = 'OCA\OtherApp\UserMigration\OtherAppMigrator'; + + $serviceRegistrationA = new ServiceRegistration($appIdA, $migratorClassA); + $serviceRegistrationB = new ServiceRegistration($appIdB, $migratorClassB); + + $this->context + ->for($appIdA) + ->registerUserMigrator($migratorClassA); + $this->context + ->for($appIdB) + ->registerUserMigrator($migratorClassB); + + $this->assertEquals( + [ + $serviceRegistrationA, + $serviceRegistrationB, + ], + $this->context->getUserMigrators(), + ); + } + + public static function dataProvider_TrueFalse(): array { + return[ + [true], + [false] + ]; + } + + public function testGetMiddlewareRegistrations(): void { + $this->context->registerMiddleware('core', TwoFactorMiddleware::class, false); + + $registrations = $this->context->getMiddlewareRegistrations(); + + self::assertNotEmpty($registrations); + self::assertSame('core', $registrations[0]->getAppId()); + self::assertSame(TwoFactorMiddleware::class, $registrations[0]->getService()); + } +} diff --git a/tests/lib/AppFramework/Controller/ApiControllerTest.php b/tests/lib/AppFramework/Controller/ApiControllerTest.php new file mode 100644 index 00000000000..9dd980f975f --- /dev/null +++ b/tests/lib/AppFramework/Controller/ApiControllerTest.php @@ -0,0 +1,43 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Controller; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\ApiController; +use OCP\IConfig; +use OCP\IRequestId; + +class ChildApiController extends ApiController { +}; + + +class ApiControllerTest extends \Test\TestCase { + /** @var ChildApiController */ + protected $controller; + + public function testCors(): void { + $request = new Request( + ['server' => ['HTTP_ORIGIN' => 'test']], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->controller = new ChildApiController('app', $request, 'verbs', + 'headers', 100); + + $response = $this->controller->preflightedCors(); + + $headers = $response->getHeaders(); + + $this->assertEquals('test', $headers['Access-Control-Allow-Origin']); + $this->assertEquals('verbs', $headers['Access-Control-Allow-Methods']); + $this->assertEquals('headers', $headers['Access-Control-Allow-Headers']); + $this->assertEquals('false', $headers['Access-Control-Allow-Credentials']); + $this->assertEquals(100, $headers['Access-Control-Max-Age']); + } +} diff --git a/tests/lib/AppFramework/Controller/AuthPublicShareControllerTest.php b/tests/lib/AppFramework/Controller/AuthPublicShareControllerTest.php new file mode 100644 index 00000000000..4efcac2dccf --- /dev/null +++ b/tests/lib/AppFramework/Controller/AuthPublicShareControllerTest.php @@ -0,0 +1,142 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Controller; + +use OCP\AppFramework\AuthPublicShareController; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; + +class AuthPublicShareControllerTest extends \Test\TestCase { + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */ + private $session; + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + private $urlGenerator; + + /** @var AuthPublicShareController|\PHPUnit\Framework\MockObject\MockObject */ + private $controller; + + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->session = $this->createMock(ISession::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->controller = $this->getMockBuilder(AuthPublicShareController::class) + ->setConstructorArgs([ + 'app', + $this->request, + $this->session, + $this->urlGenerator + ])->onlyMethods([ + 'authFailed', + 'getPasswordHash', + 'isAuthenticated', + 'isPasswordProtected', + 'isValidToken', + 'showShare', + 'verifyPassword', + 'validateIdentity', + 'generatePassword' + ])->getMock(); + } + + public function testShowAuthenticate(): void { + $expects = new TemplateResponse('core', 'publicshareauth', [], 'guest'); + + $this->assertEquals($expects, $this->controller->showAuthenticate()); + } + + public function testAuthenticateAuthenticated(): void { + $this->controller->method('isAuthenticated') + ->willReturn(true); + + $this->controller->setToken('myToken'); + + $this->session->method('get') + ->willReturnMap([ + ['public_link_authenticate_redirect', json_encode(['foo' => 'bar'])], + ]); + + $this->urlGenerator->method('linkToRoute') + ->willReturn('myLink!'); + + $result = $this->controller->authenticate('password'); + $this->assertInstanceOf(RedirectResponse::class, $result); + $this->assertSame('myLink!', $result->getRedirectURL()); + } + + public function testAuthenticateInvalidPassword(): void { + $this->controller->setToken('token'); + $this->controller->method('isPasswordProtected') + ->willReturn(true); + + $this->controller->method('verifyPassword') + ->with('password') + ->willReturn(false); + + $this->controller->expects($this->once()) + ->method('authFailed'); + + $expects = new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest'); + $expects->throttle(); + + $result = $this->controller->authenticate('password'); + + $this->assertEquals($expects, $result); + } + + public function testAuthenticateValidPassword(): void { + $this->controller->setToken('token'); + $this->controller->method('isPasswordProtected') + ->willReturn(true); + $this->controller->method('verifyPassword') + ->with('password') + ->willReturn(true); + $this->controller->method('getPasswordHash') + ->willReturn('hash'); + + $this->session->expects($this->once()) + ->method('regenerateId'); + $this->session->method('get') + ->willReturnMap([ + ['public_link_authenticate_redirect', json_encode(['foo' => 'bar'])], + ]); + + $tokenSet = false; + $hashSet = false; + $this->session + ->method('set') + ->willReturnCallback(function ($key, $value) use (&$tokenSet, &$hashSet) { + if ($key === 'public_link_authenticated_token' && $value === 'token') { + $tokenSet = true; + return true; + } + if ($key === 'public_link_authenticated_password_hash' && $value === 'hash') { + $hashSet = true; + return true; + } + return false; + }); + + $this->urlGenerator->method('linkToRoute') + ->willReturn('myLink!'); + + $result = $this->controller->authenticate('password'); + $this->assertInstanceOf(RedirectResponse::class, $result); + $this->assertSame('myLink!', $result->getRedirectURL()); + $this->assertTrue($tokenSet); + $this->assertTrue($hashSet); + } +} diff --git a/tests/lib/AppFramework/Controller/ControllerTest.php b/tests/lib/AppFramework/Controller/ControllerTest.php new file mode 100644 index 00000000000..aa016872847 --- /dev/null +++ b/tests/lib/AppFramework/Controller/ControllerTest.php @@ -0,0 +1,147 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Controller; + +use OC\AppFramework\DependencyInjection\DIContainer; +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IRequestId; + +class ChildController extends Controller { + public function __construct($appName, $request) { + parent::__construct($appName, $request); + $this->registerResponder('tom', function ($respone) { + return 'hi'; + }); + } + + public function custom($in) { + $this->registerResponder('json', function ($response) { + return new JSONResponse([strlen($response)]); + }); + + return $in; + } + + public function customDataResponse($in) { + $response = new DataResponse($in, 300); + $response->addHeader('test', 'something'); + return $response; + } +}; + +class ControllerTest extends \Test\TestCase { + /** + * @var Controller + */ + private $controller; + private $app; + private $request; + + protected function setUp(): void { + parent::setUp(); + + $request = new Request( + [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'post' => ['name' => 'Jane Doe', 'nickname' => 'Janey'], + 'urlParams' => ['name' => 'Johnny Weissmüller'], + 'files' => ['file' => 'filevalue'], + 'env' => ['PATH' => 'daheim'], + 'session' => ['sezession' => 'kein'], + 'method' => 'hi', + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + + $this->app = $this->getMockBuilder(DIContainer::class) + ->onlyMethods(['getAppName']) + ->setConstructorArgs(['test']) + ->getMock(); + $this->app->expects($this->any()) + ->method('getAppName') + ->willReturn('apptemplate_advanced'); + + $this->controller = new ChildController($this->app, $request); + $this->overwriteService(IRequest::class, $request); + $this->request = $request; + } + + + public function testFormatResonseInvalidFormat(): void { + $this->expectException(\DomainException::class); + + $this->controller->buildResponse(null, 'test'); + } + + + public function testFormat(): void { + $response = $this->controller->buildResponse(['hi'], 'json'); + + $this->assertEquals(['hi'], $response->getData()); + } + + + public function testFormatDataResponseJSON(): void { + $expectedHeaders = [ + 'test' => 'something', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + 'Content-Type' => 'application/json; charset=utf-8', + 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'", + 'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'", + 'X-Request-Id' => $this->request->getId(), + 'X-Robots-Tag' => 'noindex, nofollow', + ]; + + $response = $this->controller->customDataResponse(['hi']); + $response = $this->controller->buildResponse($response, 'json'); + + $this->assertEquals(['hi'], $response->getData()); + $this->assertEquals(300, $response->getStatus()); + $this->assertEquals($expectedHeaders, $response->getHeaders()); + } + + + public function testCustomFormatter(): void { + $response = $this->controller->custom('hi'); + $response = $this->controller->buildResponse($response, 'json'); + + $this->assertEquals([2], $response->getData()); + } + + + public function testDefaultResponderToJSON(): void { + $responder = $this->controller->getResponderByHTTPHeader('*/*'); + + $this->assertEquals('json', $responder); + } + + + public function testResponderAcceptHeaderParsed(): void { + $responder = $this->controller->getResponderByHTTPHeader( + '*/*, application/tom, application/json' + ); + + $this->assertEquals('tom', $responder); + } + + + public function testResponderAcceptHeaderParsedUpperCase(): void { + $responder = $this->controller->getResponderByHTTPHeader( + '*/*, apPlication/ToM, application/json' + ); + + $this->assertEquals('tom', $responder); + } +} diff --git a/tests/lib/AppFramework/Controller/OCSControllerTest.php b/tests/lib/AppFramework/Controller/OCSControllerTest.php new file mode 100644 index 00000000000..4ab45ad6b06 --- /dev/null +++ b/tests/lib/AppFramework/Controller/OCSControllerTest.php @@ -0,0 +1,135 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Controller; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; +use OCP\AppFramework\OCSController; +use OCP\IConfig; +use OCP\IRequestId; + +class ChildOCSController extends OCSController { +} + + +class OCSControllerTest extends \Test\TestCase { + public function testCors(): void { + $request = new Request( + [ + 'server' => [ + 'HTTP_ORIGIN' => 'test', + ], + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $controller = new ChildOCSController('app', $request, 'verbs', + 'headers', 100); + + $response = $controller->preflightedCors(); + + $headers = $response->getHeaders(); + + $this->assertEquals('test', $headers['Access-Control-Allow-Origin']); + $this->assertEquals('verbs', $headers['Access-Control-Allow-Methods']); + $this->assertEquals('headers', $headers['Access-Control-Allow-Headers']); + $this->assertEquals('false', $headers['Access-Control-Allow-Credentials']); + $this->assertEquals(100, $headers['Access-Control-Max-Age']); + } + + + public function testXML(): void { + $controller = new ChildOCSController('app', new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + )); + $controller->setOCSVersion(1); + + $expected = "<?xml version=\"1.0\"?>\n" + . "<ocs>\n" + . " <meta>\n" + . " <status>ok</status>\n" + . " <statuscode>100</statuscode>\n" + . " <message>OK</message>\n" + . " <totalitems></totalitems>\n" + . " <itemsperpage></itemsperpage>\n" + . " </meta>\n" + . " <data>\n" + . " <test>hi</test>\n" + . " </data>\n" + . "</ocs>\n"; + + $params = new DataResponse(['test' => 'hi']); + + $response = $controller->buildResponse($params, 'xml'); + $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); + $this->assertEquals($expected, $response->render()); + } + + public function testJSON(): void { + $controller = new ChildOCSController('app', new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + )); + $controller->setOCSVersion(1); + $expected = '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK",' + . '"totalitems":"","itemsperpage":""},"data":{"test":"hi"}}}'; + $params = new DataResponse(['test' => 'hi']); + + $response = $controller->buildResponse($params, 'json'); + $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); + $this->assertEquals($expected, $response->render()); + $this->assertEquals($expected, $response->render()); + } + + public function testXMLV2(): void { + $controller = new ChildOCSController('app', new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + )); + $controller->setOCSVersion(2); + + $expected = "<?xml version=\"1.0\"?>\n" + . "<ocs>\n" + . " <meta>\n" + . " <status>ok</status>\n" + . " <statuscode>200</statuscode>\n" + . " <message>OK</message>\n" + . " </meta>\n" + . " <data>\n" + . " <test>hi</test>\n" + . " </data>\n" + . "</ocs>\n"; + + $params = new DataResponse(['test' => 'hi']); + + $response = $controller->buildResponse($params, 'xml'); + $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); + $this->assertEquals($expected, $response->render()); + } + + public function testJSONV2(): void { + $controller = new ChildOCSController('app', new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + )); + $controller->setOCSVersion(2); + $expected = '{"ocs":{"meta":{"status":"ok","statuscode":200,"message":"OK"},"data":{"test":"hi"}}}'; + $params = new DataResponse(['test' => 'hi']); + + $response = $controller->buildResponse($params, 'json'); + $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); + $this->assertEquals($expected, $response->render()); + } +} diff --git a/tests/lib/AppFramework/Controller/PublicShareControllerTest.php b/tests/lib/AppFramework/Controller/PublicShareControllerTest.php new file mode 100644 index 00000000000..e676b8a0d7e --- /dev/null +++ b/tests/lib/AppFramework/Controller/PublicShareControllerTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Controller; + +use OCP\AppFramework\PublicShareController; +use OCP\IRequest; +use OCP\ISession; + +class TestController extends PublicShareController { + public function __construct( + string $appName, + IRequest $request, + ISession $session, + private string $hash, + private bool $isProtected, + ) { + parent::__construct($appName, $request, $session); + } + + protected function getPasswordHash(): string { + return $this->hash; + } + + public function isValidToken(): bool { + return false; + } + + protected function isPasswordProtected(): bool { + return $this->isProtected; + } +} + +class PublicShareControllerTest extends \Test\TestCase { + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */ + private $session; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->session = $this->createMock(ISession::class); + } + + public function testGetToken(): void { + $controller = new TestController('app', $this->request, $this->session, 'hash', false); + + $controller->setToken('test'); + $this->assertEquals('test', $controller->getToken()); + } + + public static function dataIsAuthenticated(): array { + return [ + [false, 'token1', 'token1', 'hash1', 'hash1', true], + [false, 'token1', 'token1', 'hash1', 'hash2', true], + [false, 'token1', 'token2', 'hash1', 'hash1', true], + [false, 'token1', 'token2', 'hash1', 'hash2', true], + [ true, 'token1', 'token1', 'hash1', 'hash1', true], + [ true, 'token1', 'token1', 'hash1', 'hash2', false], + [ true, 'token1', 'token2', 'hash1', 'hash1', false], + [ true, 'token1', 'token2', 'hash1', 'hash2', false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataIsAuthenticated')] + public function testIsAuthenticatedNotPasswordProtected(bool $protected, string $token1, string $token2, string $hash1, string $hash2, bool $expected): void { + $controller = new TestController('app', $this->request, $this->session, $hash2, $protected); + + $this->session->method('get') + ->willReturnMap([ + ['public_link_authenticated_token', $token1], + ['public_link_authenticated_password_hash', $hash1], + ]); + + $controller->setToken($token2); + + $this->assertEquals($expected, $controller->isAuthenticated()); + } +} diff --git a/tests/lib/AppFramework/Db/EntityTest.php b/tests/lib/AppFramework/Db/EntityTest.php new file mode 100644 index 00000000000..eab081e6ac6 --- /dev/null +++ b/tests/lib/AppFramework/Db/EntityTest.php @@ -0,0 +1,320 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; +use PHPUnit\Framework\Constraint\IsType; + +/** + * @method integer getId() + * @method void setId(integer $id) + * @method integer getTestId() + * @method void setTestId(integer $id) + * @method string getName() + * @method void setName(string $name) + * @method string getEmail() + * @method void setEmail(string $email) + * @method string getPreName() + * @method void setPreName(string $preName) + * @method bool getTrueOrFalse() + * @method bool isTrueOrFalse() + * @method void setTrueOrFalse(bool $trueOrFalse) + * @method bool getAnotherBool() + * @method bool isAnotherBool() + * @method string getLongText() + * @method void setLongText(string $longText) + * @method \DateTime getTime() + * @method void setTime(\DateTime $time) + * @method \DateTimeImmutable getDatetime() + * @method void setDatetime(\DateTimeImmutable $datetime) + */ +class TestEntity extends Entity { + protected $email; + protected $testId; + protected $smallInt; + protected $bigInt; + protected $preName; + protected $trueOrFalse; + protected $anotherBool; + protected $text; + protected $longText; + protected $time; + protected $datetime; + + public function __construct( + protected $name = null, + ) { + $this->addType('testId', Types::INTEGER); + $this->addType('smallInt', Types::SMALLINT); + $this->addType('bigInt', Types::BIGINT); + $this->addType('anotherBool', Types::BOOLEAN); + $this->addType('text', Types::TEXT); + $this->addType('longText', Types::BLOB); + $this->addType('time', Types::TIME); + $this->addType('datetime', Types::DATETIME_IMMUTABLE); + + // Legacy types + $this->addType('trueOrFalse', 'bool'); + $this->addType('legacyInt', 'int'); + $this->addType('doubleNowFloat', 'double'); + } + + public function setAnotherBool(bool $anotherBool): void { + parent::setAnotherBool($anotherBool); + } +} + + +class EntityTest extends \Test\TestCase { + private $entity; + + protected function setUp(): void { + parent::setUp(); + $this->entity = new TestEntity(); + } + + + public function testResetUpdatedFields(): void { + $entity = new TestEntity(); + $entity->setId(3); + $entity->resetUpdatedFields(); + + $this->assertEquals([], $entity->getUpdatedFields()); + } + + + public function testFromRow(): void { + $row = [ + 'pre_name' => 'john', + 'email' => 'john@something.com', + 'another_bool' => 1, + ]; + $this->entity = TestEntity::fromRow($row); + + $this->assertEquals($row['pre_name'], $this->entity->getPreName()); + $this->assertEquals($row['email'], $this->entity->getEmail()); + $this->assertEquals($row['another_bool'], $this->entity->getAnotherBool()); + } + + + public function testGetSetId(): void { + $id = 3; + $this->entity->setId(3); + + $this->assertEquals($id, $this->entity->getId()); + } + + + public function testColumnToPropertyNoReplacement(): void { + $column = 'my'; + $this->assertEquals('my', + $this->entity->columnToProperty($column)); + } + + + public function testColumnToProperty(): void { + $column = 'my_attribute'; + $this->assertEquals('myAttribute', + $this->entity->columnToProperty($column)); + } + + + public function testPropertyToColumnNoReplacement(): void { + $property = 'my'; + $this->assertEquals('my', + $this->entity->propertyToColumn($property)); + } + + + public function testSetterMarksFieldUpdated(): void { + $this->entity->setId(3); + + $this->assertContains('id', array_keys($this->entity->getUpdatedFields())); + } + + + + public function testCallShouldOnlyWorkForGetterSetter(): void { + $this->expectException(\BadFunctionCallException::class); + + $this->entity->something(); + } + + + + public function testGetterShouldFailIfAttributeNotDefined(): void { + $this->expectException(\BadFunctionCallException::class); + + $this->entity->getTest(); + } + + + public function testSetterShouldFailIfAttributeNotDefined(): void { + $this->expectException(\BadFunctionCallException::class); + + $this->entity->setTest(); + } + + + public function testFromRowShouldNotAssignEmptyArray(): void { + $row = []; + $entity2 = new TestEntity(); + + $this->entity = TestEntity::fromRow($row); + $this->assertEquals($entity2, $this->entity); + } + + + public function testIdGetsConvertedToInt(): void { + $row = ['id' => '4']; + + $this->entity = TestEntity::fromRow($row); + $this->assertSame(4, $this->entity->getId()); + } + + + public function testSetType(): void { + $row = ['testId' => '4']; + + $this->entity = TestEntity::fromRow($row); + $this->assertSame(4, $this->entity->getTestId()); + } + + + public function testFromParams(): void { + $params = [ + 'testId' => 4, + 'email' => 'john@doe' + ]; + + $entity = TestEntity::fromParams($params); + + $this->assertEquals($params['testId'], $entity->getTestId()); + $this->assertEquals($params['email'], $entity->getEmail()); + $this->assertTrue($entity instanceof TestEntity); + } + + public function testSlugify(): void { + $entity = new TestEntity(); + $entity->setName('Slugify this!'); + $this->assertEquals('slugify-this', $entity->slugify('name')); + $entity->setName('°!"§$%&/()=?`´ß\}][{³²#\'+~*-_.:,;<>|äöüÄÖÜSlugify this!'); + $this->assertEquals('slugify-this', $entity->slugify('name')); + } + + + public static function dataSetterCasts(): array { + return [ + ['Id', '3', 3], + ['smallInt', '3', 3], + ['bigInt', '' . PHP_INT_MAX, PHP_INT_MAX], + ['trueOrFalse', 0, false], + ['trueOrFalse', 1, true], + ['anotherBool', 0, false], + ['anotherBool', 1, true], + ['text', 33, '33'], + ['longText', PHP_INT_MAX, '' . PHP_INT_MAX], + ]; + } + + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSetterCasts')] + public function testSetterCasts(string $field, mixed $in, mixed $out): void { + $entity = new TestEntity(); + $entity->{'set' . $field}($in); + $this->assertSame($out, $entity->{'get' . $field}()); + } + + + public function testSetterDoesNotCastOnNull(): void { + $entity = new TestEntity(); + $entity->setId(null); + $this->assertSame(null, $entity->getId()); + } + + public function testSetterConvertsResourcesToStringProperly(): void { + $string = 'Definitely a string'; + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $string); + rewind($stream); + + $entity = new TestEntity(); + $entity->setLongText($stream); + fclose($stream); + $this->assertSame($string, $entity->getLongText()); + } + + public function testSetterConvertsDatetime() { + $entity = new TestEntity(); + $entity->setDatetime('2024-08-19 15:26:00'); + $this->assertEquals(new \DateTimeImmutable('2024-08-19 15:26:00'), $entity->getDatetime()); + } + + public function testSetterDoesNotConvertNullOnDatetime() { + $entity = new TestEntity(); + $entity->setDatetime(null); + $this->assertNull($entity->getDatetime()); + } + + public function testSetterConvertsTime() { + $entity = new TestEntity(); + $entity->setTime('15:26:00'); + $this->assertEquals(new \DateTime('15:26:00'), $entity->getTime()); + } + + public function testGetFieldTypes(): void { + $entity = new TestEntity(); + $this->assertEquals([ + 'id' => Types::INTEGER, + 'testId' => Types::INTEGER, + 'smallInt' => Types::SMALLINT, + 'bigInt' => Types::BIGINT, + 'anotherBool' => Types::BOOLEAN, + 'text' => Types::TEXT, + 'longText' => Types::BLOB, + 'time' => Types::TIME, + 'datetime' => Types::DATETIME_IMMUTABLE, + 'trueOrFalse' => Types::BOOLEAN, + 'legacyInt' => Types::INTEGER, + 'doubleNowFloat' => Types::FLOAT, + ], $entity->getFieldTypes()); + } + + + public function testGetItInt(): void { + $entity = new TestEntity(); + $entity->setId(3); + $this->assertEquals(Types::INTEGER, gettype($entity->getId())); + } + + + public function testFieldsNotMarkedUpdatedIfNothingChanges(): void { + $entity = new TestEntity('hey'); + $entity->setName('hey'); + $this->assertEquals(0, count($entity->getUpdatedFields())); + } + + public function testIsGetter(): void { + $entity = new TestEntity(); + $entity->setTrueOrFalse(false); + $entity->setAnotherBool(false); + $this->assertThat($entity->isTrueOrFalse(), new IsType(IsType::TYPE_BOOL)); + $this->assertThat($entity->isAnotherBool(), new IsType(IsType::TYPE_BOOL)); + } + + + public function testIsGetterShoudFailForOtherType(): void { + $this->expectException(\BadFunctionCallException::class); + + $entity = new TestEntity(); + $entity->setName('hello'); + $this->assertThat($entity->isName(), new IsType(IsType::TYPE_BOOL)); + } +} diff --git a/tests/lib/AppFramework/Db/QBMapperDBTest.php b/tests/lib/AppFramework/Db/QBMapperDBTest.php new file mode 100644 index 00000000000..614f1099644 --- /dev/null +++ b/tests/lib/AppFramework/Db/QBMapperDBTest.php @@ -0,0 +1,160 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Db; + +use Doctrine\DBAL\Schema\SchemaException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\Types; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Server; +use Test\TestCase; + +/** + * @method void setTime(?\DateTime $time) + * @method ?\DateTime getTime() + * @method void setDatetime(?\DateTimeImmutable $datetime) + * @method ?\DateTimeImmutable getDatetime() + */ +class QBDBTestEntity extends Entity { + protected ?\DateTime $time = null; + protected ?\DateTimeImmutable $datetime = null; + + public function __construct() { + $this->addType('time', Types::TIME); + $this->addType('datetime', Types::DATETIME_IMMUTABLE); + } +} + +/** + * Class QBDBTestMapper + * + * @package Test\AppFramework\Db + */ +class QBDBTestMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'testing', QBDBTestEntity::class); + } + + public function getParameterTypeForPropertyForTest(Entity $entity, string $property) { + return parent::getParameterTypeForProperty($entity, $property); + } + + public function getById(int $id): QBDBTestEntity { + $qb = $this->db->getQueryBuilder(); + $query = $qb + ->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('id', $qb->createPositionalParameter($id, IQueryBuilder::PARAM_INT)), + ); + return $this->findEntity($query); + } +} + +/** + * Test real database handling (serialization) + * @group DB + */ +class QBMapperDBTest extends TestCase { + /** @var \Doctrine\DBAL\Connection|IDBConnection */ + protected $connection; + protected $schemaSetup = false; + + protected function setUp(): void { + parent::setUp(); + + $this->connection = Server::get(IDBConnection::class); + $this->prepareTestingTable(); + } + + public function testInsertDateTime(): void { + $mapper = new QBDBTestMapper($this->connection); + $entity = new QBDBTestEntity(); + $entity->setTime(new \DateTime('2003-01-01 12:34:00')); + $entity->setDatetime(new \DateTimeImmutable('2000-01-01 23:45:00')); + + $result = $mapper->insert($entity); + $this->assertNotNull($result->getId()); + } + + public function testRetrieveDateTime(): void { + $time = new \DateTime('2000-01-01 01:01:00'); + $datetime = new \DateTimeImmutable('2000-01-01 02:02:00'); + + $mapper = new QBDBTestMapper($this->connection); + $entity = new QBDBTestEntity(); + $entity->setTime($time); + $entity->setDatetime($datetime); + + $result = $mapper->insert($entity); + $this->assertNotNull($result->getId()); + + $dbEntity = $mapper->getById($result->getId()); + $this->assertEquals($time->format('H:i:s'), $dbEntity->getTime()->format('H:i:s')); + $this->assertEquals($datetime->format('Y-m-d H:i:s'), $dbEntity->getDatetime()->format('Y-m-d H:i:s')); + // The date is not saved for "time" + $this->assertNotEquals($time->format('Y'), $dbEntity->getTime()->format('Y')); + } + + public function testUpdateDateTime(): void { + $time = new \DateTime('2000-01-01 01:01:00'); + $datetime = new \DateTimeImmutable('2000-01-01 02:02:00'); + + $mapper = new QBDBTestMapper($this->connection); + $entity = new QBDBTestEntity(); + $entity->setTime('now'); + $entity->setDatetime('now'); + + /** @var QBDBTestEntity */ + $entity = $mapper->insert($entity); + $this->assertNotNull($entity->getId()); + + // Update the values + $entity->setTime($time); + $entity->setDatetime($datetime); + $mapper->update($entity); + + $dbEntity = $mapper->getById($entity->getId()); + $this->assertEquals($time->format('H:i:s'), $dbEntity->getTime()->format('H:i:s')); + $this->assertEquals($datetime->format('Y-m-d H:i:s'), $dbEntity->getDatetime()->format('Y-m-d H:i:s')); + } + + protected function prepareTestingTable(): void { + if ($this->schemaSetup) { + $this->connection->getQueryBuilder()->delete('testing')->executeStatement(); + } + + $prefix = Server::get(IConfig::class)->getSystemValueString('dbtableprefix', 'oc_'); + $schema = $this->connection->createSchema(); + try { + $schema->getTable($prefix . 'testing'); + $this->connection->getQueryBuilder()->delete('testing')->executeStatement(); + } catch (SchemaException $e) { + $this->schemaSetup = true; + $table = $schema->createTable($prefix . 'testing'); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + + $table->addColumn('time', Types::TIME, [ + 'notnull' => false, + ]); + + $table->addColumn('datetime', Types::DATETIME_IMMUTABLE, [ + 'notnull' => false, + ]); + + $table->setPrimaryKey(['id']); + $this->connection->migrateToSchema($schema); + } + } +} diff --git a/tests/lib/AppFramework/Db/QBMapperTest.php b/tests/lib/AppFramework/Db/QBMapperTest.php new file mode 100644 index 00000000000..0f18ef3f204 --- /dev/null +++ b/tests/lib/AppFramework/Db/QBMapperTest.php @@ -0,0 +1,245 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\Types; +use OCP\IDBConnection; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * @method bool getBoolProp() + * @method void setBoolProp(bool $boolProp) + * @method integer getIntProp() + * @method void setIntProp(integer $intProp) + * @method string getStringProp() + * @method void setStringProp(string $stringProp) + * @method bool getBooleanProp() + * @method void setBooleanProp(bool $booleanProp) + * @method integer getIntegerProp() + * @method void setIntegerProp(integer $integerProp) + * @method ?\DateTimeImmutable getDatetimeProp() + * @method void setDatetimeProp(?\DateTimeImmutable $datetime) + */ +class QBTestEntity extends Entity { + protected $intProp; + protected $boolProp; + protected $stringProp; + protected $integerProp; + protected $booleanProp; + protected $jsonProp; + protected $datetimeProp; + + public function __construct() { + $this->addType('intProp', 'int'); + $this->addType('boolProp', 'bool'); + $this->addType('stringProp', Types::STRING); + $this->addType('integerProp', Types::INTEGER); + $this->addType('booleanProp', Types::BOOLEAN); + $this->addType('jsonProp', Types::JSON); + $this->addType('datetimeProp', Types::DATETIME_IMMUTABLE); + } +} + +/** + * Class QBTestMapper + * + * @package Test\AppFramework\Db + */ +class QBTestMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'table'); + } + + public function getParameterTypeForPropertyForTest(Entity $entity, string $property) { + return parent::getParameterTypeForProperty($entity, $property); + } +} + +/** + * Class QBMapperTest + * + * @package Test\AppFramework\Db + */ +class QBMapperTest extends \Test\TestCase { + + protected IDBConnection&MockObject $db; + protected IQueryBuilder&MockObject $qb; + protected IExpressionBuilder&MockObject $expr; + protected QBTestMapper $mapper; + + /** + * @throws \ReflectionException + */ + protected function setUp(): void { + $this->db = $this->getMockBuilder(IDBConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->qb = $this->getMockBuilder(IQueryBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->expr = $this->getMockBuilder(IExpressionBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + + $this->qb->method('expr')->willReturn($this->expr); + $this->db->method('getQueryBuilder')->willReturn($this->qb); + + + $this->mapper = new QBTestMapper($this->db); + } + + + public function testInsertEntityParameterTypeMapping(): void { + $datetime = new \DateTimeImmutable(); + $entity = new QBTestEntity(); + $entity->setIntProp(123); + $entity->setBoolProp(true); + $entity->setStringProp('string'); + $entity->setIntegerProp(456); + $entity->setBooleanProp(false); + $entity->setDatetimeProp($datetime); + + $intParam = $this->qb->createNamedParameter('int_prop', IQueryBuilder::PARAM_INT); + $boolParam = $this->qb->createNamedParameter('bool_prop', IQueryBuilder::PARAM_BOOL); + $stringParam = $this->qb->createNamedParameter('string_prop', IQueryBuilder::PARAM_STR); + $integerParam = $this->qb->createNamedParameter('integer_prop', IQueryBuilder::PARAM_INT); + $booleanParam = $this->qb->createNamedParameter('boolean_prop', IQueryBuilder::PARAM_BOOL); + $datetimeParam = $this->qb->createNamedParameter('datetime_prop', IQueryBuilder::PARAM_DATETIME_IMMUTABLE); + + $createNamedParameterCalls = [ + [123, IQueryBuilder::PARAM_INT, null], + [true, IQueryBuilder::PARAM_BOOL, null], + ['string', IQueryBuilder::PARAM_STR, null], + [456, IQueryBuilder::PARAM_INT, null], + [false, IQueryBuilder::PARAM_BOOL, null], + [$datetime, IQueryBuilder::PARAM_DATETIME_IMMUTABLE, null], + ]; + $this->qb->expects($this->exactly(6)) + ->method('createNamedParameter') + ->willReturnCallback(function () use (&$createNamedParameterCalls): void { + $expected = array_shift($createNamedParameterCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $setValueCalls = [ + ['int_prop', $intParam], + ['bool_prop', $boolParam], + ['string_prop', $stringParam], + ['integer_prop', $integerParam], + ['boolean_prop', $booleanParam], + ['datetime_prop', $datetimeParam], + ]; + $this->qb->expects($this->exactly(6)) + ->method('setValue') + ->willReturnCallback(function () use (&$setValueCalls): void { + $expected = array_shift($setValueCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->mapper->insert($entity); + } + + + public function testUpdateEntityParameterTypeMapping(): void { + $datetime = new \DateTimeImmutable(); + $entity = new QBTestEntity(); + $entity->setId(789); + $entity->setIntProp(123); + $entity->setBoolProp('true'); + $entity->setStringProp('string'); + $entity->setIntegerProp(456); + $entity->setBooleanProp(false); + $entity->setJsonProp(['hello' => 'world']); + $entity->setDatetimeProp($datetime); + + $idParam = $this->qb->createNamedParameter('id', IQueryBuilder::PARAM_INT); + $intParam = $this->qb->createNamedParameter('int_prop', IQueryBuilder::PARAM_INT); + $boolParam = $this->qb->createNamedParameter('bool_prop', IQueryBuilder::PARAM_BOOL); + $stringParam = $this->qb->createNamedParameter('string_prop', IQueryBuilder::PARAM_STR); + $integerParam = $this->qb->createNamedParameter('integer_prop', IQueryBuilder::PARAM_INT); + $booleanParam = $this->qb->createNamedParameter('boolean_prop', IQueryBuilder::PARAM_BOOL); + $jsonParam = $this->qb->createNamedParameter('json_prop', IQueryBuilder::PARAM_JSON); + $datetimeParam = $this->qb->createNamedParameter('datetime_prop', IQueryBuilder::PARAM_DATETIME_IMMUTABLE); + + $createNamedParameterCalls = [ + [123, IQueryBuilder::PARAM_INT, null], + [true, IQueryBuilder::PARAM_BOOL, null], + ['string', IQueryBuilder::PARAM_STR, null], + [456, IQueryBuilder::PARAM_INT, null], + [false, IQueryBuilder::PARAM_BOOL, null], + [['hello' => 'world'], IQueryBuilder::PARAM_JSON, null], + [$datetime, IQueryBuilder::PARAM_DATETIME_IMMUTABLE, null], + [789, IQueryBuilder::PARAM_INT, null], + ]; + $this->qb->expects($this->exactly(8)) + ->method('createNamedParameter') + ->willReturnCallback(function () use (&$createNamedParameterCalls): void { + $expected = array_shift($createNamedParameterCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $setCalls = [ + ['int_prop', $intParam], + ['bool_prop', $boolParam], + ['string_prop', $stringParam], + ['integer_prop', $integerParam], + ['boolean_prop', $booleanParam], + ['json_prop', $datetimeParam], + ['datetime_prop', $datetimeParam], + ]; + $this->qb->expects($this->exactly(7)) + ->method('set') + ->willReturnCallback(function () use (&$setCalls): void { + $expected = array_shift($setCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->expr->expects($this->once()) + ->method('eq') + ->with($this->equalTo('id'), $this->equalTo($idParam)); + + + $this->mapper->update($entity); + } + + + public function testGetParameterTypeForProperty(): void { + $entity = new QBTestEntity(); + + $intType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'intProp'); + $this->assertEquals(IQueryBuilder::PARAM_INT, $intType, 'Int type property mapping incorrect'); + + $integerType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'integerProp'); + $this->assertEquals(IQueryBuilder::PARAM_INT, $integerType, 'Integer type property mapping incorrect'); + + $boolType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'boolProp'); + $this->assertEquals(IQueryBuilder::PARAM_BOOL, $boolType, 'Bool type property mapping incorrect'); + + $booleanType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'booleanProp'); + $this->assertEquals(IQueryBuilder::PARAM_BOOL, $booleanType, 'Boolean type property mapping incorrect'); + + $stringType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'stringProp'); + $this->assertEquals(IQueryBuilder::PARAM_STR, $stringType, 'String type property mapping incorrect'); + + $jsonType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'jsonProp'); + $this->assertEquals(IQueryBuilder::PARAM_JSON, $jsonType, 'JSON type property mapping incorrect'); + + $datetimeType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'datetimeProp'); + $this->assertEquals(IQueryBuilder::PARAM_DATETIME_IMMUTABLE, $datetimeType, 'DateTimeImmutable type property mapping incorrect'); + + $unknownType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'someProp'); + $this->assertEquals(IQueryBuilder::PARAM_STR, $unknownType, 'Unknown type property mapping incorrect'); + } +} diff --git a/tests/lib/AppFramework/Db/TransactionalTest.php b/tests/lib/AppFramework/Db/TransactionalTest.php new file mode 100644 index 00000000000..72a3d9ae59f --- /dev/null +++ b/tests/lib/AppFramework/Db/TransactionalTest.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\AppFramework\Db; + +use OCP\AppFramework\Db\TTransactional; +use OCP\IDBConnection; +use PHPUnit\Framework\MockObject\MockObject; +use RuntimeException; +use Test\TestCase; + +class TransactionalTest extends TestCase { + /** @var IDBConnection|MockObject */ + private IDBConnection $db; + + protected function setUp(): void { + parent::setUp(); + + $this->db = $this->createMock(IDBConnection::class); + } + + public function testAtomicRollback(): void { + $test = new class($this->db) { + use TTransactional; + + public function __construct( + private IDBConnection $db, + ) { + } + + public function fail(): void { + $this->atomic(function (): void { + throw new RuntimeException('nope'); + }, $this->db); + } + }; + $this->db->expects(self::once()) + ->method('beginTransaction'); + $this->db->expects(self::once()) + ->method('rollback'); + $this->db->expects(self::never()) + ->method('commit'); + $this->expectException(RuntimeException::class); + + $test->fail(); + } + + public function testAtomicCommit(): void { + $test = new class($this->db) { + use TTransactional; + + public function __construct( + private IDBConnection $db, + ) { + } + + public function succeed(): int { + return $this->atomic(function () { + return 3; + }, $this->db); + } + }; + $this->db->expects(self::once()) + ->method('beginTransaction'); + $this->db->expects(self::never()) + ->method('rollback'); + $this->db->expects(self::once()) + ->method('commit'); + + $result = $test->succeed(); + + self::assertEquals(3, $result); + } +} diff --git a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php new file mode 100644 index 00000000000..31188b12f14 --- /dev/null +++ b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php @@ -0,0 +1,144 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\DependencyInjection; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\AppFramework\Bootstrap\MiddlewareRegistration; +use OC\AppFramework\Bootstrap\RegistrationContext; +use OC\AppFramework\DependencyInjection\DIContainer; +use OC\AppFramework\Http\Request; +use OC\AppFramework\Middleware\Security\SecurityMiddleware; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\QueryException; +use OCP\IConfig; +use OCP\IRequestId; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * @group DB + */ +class DIContainerTest extends \Test\TestCase { + private DIContainer&MockObject $container; + + protected function setUp(): void { + parent::setUp(); + $this->container = $this->getMockBuilder(DIContainer::class) + ->onlyMethods(['isAdminUser']) + ->setConstructorArgs(['name']) + ->getMock(); + } + + + public function testProvidesRequest(): void { + $this->assertTrue(isset($this->container['Request'])); + } + + public function testProvidesMiddlewareDispatcher(): void { + $this->assertTrue(isset($this->container['MiddlewareDispatcher'])); + } + + public function testProvidesAppName(): void { + $this->assertTrue(isset($this->container['AppName'])); + $this->assertTrue(isset($this->container['appName'])); + } + + + public function testAppNameIsSetCorrectly(): void { + $this->assertEquals('name', $this->container['AppName']); + $this->assertEquals('name', $this->container['appName']); + } + + public function testMiddlewareDispatcherIncludesSecurityMiddleware(): void { + $this->container['Request'] = new Request( + ['method' => 'GET'], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $dispatcher = $this->container['MiddlewareDispatcher']; + $middlewares = $dispatcher->getMiddlewares(); + + $found = false; + foreach ($middlewares as $middleware) { + if ($middleware instanceof SecurityMiddleware) { + $found = true; + } + } + + $this->assertTrue($found); + } + + public function testMiddlewareDispatcherIncludesBootstrapMiddlewares(): void { + $coordinator = $this->createMock(Coordinator::class); + $this->container[Coordinator::class] = $coordinator; + $this->container['Request'] = $this->createMock(Request::class); + $registrationContext = $this->createMock(RegistrationContext::class); + $registrationContext->method('getMiddlewareRegistrations') + ->willReturn([ + new MiddlewareRegistration($this->container['appName'], 'foo', false), + new MiddlewareRegistration('otherapp', 'bar', false), + ]); + $this->container['foo'] = new class extends Middleware { + }; + $this->container['bar'] = new class extends Middleware { + }; + $coordinator->method('getRegistrationContext')->willReturn($registrationContext); + + $dispatcher = $this->container['MiddlewareDispatcher']; + + $middlewares = $dispatcher->getMiddlewares(); + self::assertNotEmpty($middlewares); + foreach ($middlewares as $middleware) { + if ($middleware === $this->container['bar']) { + $this->fail('Container must not register this middleware'); + } + if ($middleware === $this->container['foo']) { + // It is done + return; + } + } + $this->fail('Bootstrap registered middleware not found'); + } + + public function testMiddlewareDispatcherIncludesGlobalBootstrapMiddlewares(): void { + $coordinator = $this->createMock(Coordinator::class); + $this->container[Coordinator::class] = $coordinator; + $this->container['Request'] = $this->createMock(Request::class); + $registrationContext = $this->createMock(RegistrationContext::class); + $registrationContext->method('getMiddlewareRegistrations') + ->willReturn([ + new MiddlewareRegistration('otherapp', 'foo', true), + new MiddlewareRegistration('otherapp', 'bar', false), + ]); + $this->container['foo'] = new class extends Middleware { + }; + $this->container['bar'] = new class extends Middleware { + }; + $coordinator->method('getRegistrationContext')->willReturn($registrationContext); + + $dispatcher = $this->container['MiddlewareDispatcher']; + + $middlewares = $dispatcher->getMiddlewares(); + self::assertNotEmpty($middlewares); + foreach ($middlewares as $middleware) { + if ($middleware === $this->container['bar']) { + $this->fail('Container must not register this middleware'); + } + if ($middleware === $this->container['foo']) { + // It is done + return; + } + } + $this->fail('Bootstrap registered middleware not found'); + } + + public function testInvalidAppClass(): void { + $this->expectException(QueryException::class); + $this->container->query('\OCA\Name\Foo'); + } +} diff --git a/tests/lib/AppFramework/DependencyInjection/DIIntergrationTests.php b/tests/lib/AppFramework/DependencyInjection/DIIntergrationTests.php new file mode 100644 index 00000000000..219fd5134ae --- /dev/null +++ b/tests/lib/AppFramework/DependencyInjection/DIIntergrationTests.php @@ -0,0 +1,121 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\DependencyInjection; + +use OC\AppFramework\DependencyInjection\DIContainer; +use OC\AppFramework\Utility\SimpleContainer; +use OC\ServerContainer; +use Test\TestCase; + +interface Interface1 { +} + +class ClassA1 implements Interface1 { +} + +class ClassA2 implements Interface1 { +} + +class ClassB { + /** + * ClassB constructor. + * + * @param Interface1 $interface1 + */ + public function __construct( + public Interface1 $interface1, + ) { + } +} + +class DIIntergrationTests extends TestCase { + /** @var DIContainer */ + private $container; + + /** @var ServerContainer */ + private $server; + + protected function setUp(): void { + parent::setUp(); + + $this->server = new ServerContainer(); + $this->container = new DIContainer('App1', [], $this->server); + } + + public function testInjectFromServer(): void { + $this->server->registerService(Interface1::class, function () { + return new ClassA1(); + }); + + $this->server->registerService(ClassB::class, function (SimpleContainer $c) { + return new ClassB( + $c->query(Interface1::class) + ); + }); + + /** @var ClassB $res */ + $res = $this->container->query(ClassB::class); + $this->assertSame(ClassA1::class, get_class($res->interface1)); + } + + public function testInjectDepFromServer(): void { + $this->server->registerService(Interface1::class, function () { + return new ClassA1(); + }); + + $this->container->registerService(ClassB::class, function (SimpleContainer $c) { + return new ClassB( + $c->query(Interface1::class) + ); + }); + + /** @var ClassB $res */ + $res = $this->container->query(ClassB::class); + $this->assertSame(ClassA1::class, get_class($res->interface1)); + } + + public function testOverwriteDepFromServer(): void { + $this->server->registerService(Interface1::class, function () { + return new ClassA1(); + }); + + $this->container->registerService(Interface1::class, function () { + return new ClassA2(); + }); + + $this->container->registerService(ClassB::class, function (SimpleContainer $c) { + return new ClassB( + $c->query(Interface1::class) + ); + }); + + /** @var ClassB $res */ + $res = $this->container->query(ClassB::class); + $this->assertSame(ClassA2::class, get_class($res->interface1)); + } + + public function testIgnoreOverwriteInServerClass(): void { + $this->server->registerService(Interface1::class, function () { + return new ClassA1(); + }); + + $this->container->registerService(Interface1::class, function () { + return new ClassA2(); + }); + + $this->server->registerService(ClassB::class, function (SimpleContainer $c) { + return new ClassB( + $c->query(Interface1::class) + ); + }); + + /** @var ClassB $res */ + $res = $this->container->query(ClassB::class); + $this->assertSame(ClassA1::class, get_class($res->interface1)); + } +} diff --git a/tests/lib/AppFramework/Http/ContentSecurityPolicyTest.php b/tests/lib/AppFramework/Http/ContentSecurityPolicyTest.php new file mode 100644 index 00000000000..75527e7eaf8 --- /dev/null +++ b/tests/lib/AppFramework/Http/ContentSecurityPolicyTest.php @@ -0,0 +1,516 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http\ContentSecurityPolicy; + +/** + * Class ContentSecurityPolicyTest + * + * @package OC\AppFramework\Http + */ +class ContentSecurityPolicyTest extends \Test\TestCase { + /** @var ContentSecurityPolicy */ + private $contentSecurityPolicy; + + protected function setUp(): void { + parent::setUp(); + $this->contentSecurityPolicy = new ContentSecurityPolicy(); + } + + public function testGetPolicyDefault(): void { + $defaultPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + $this->assertSame($defaultPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' www.nextcloud.com;style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' www.nextcloud.com www.nextcloud.org;style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowScriptDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowScriptDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' www.nextcloud.com;style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowScriptDomainMultipleStacked(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org')->disallowScriptDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptDisallowEval(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->allowEvalScript(false); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com www.nextcloud.org 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowStyleDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowStyleDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowStyleDomainMultipleStacked(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org')->disallowStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleAllowInline(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->allowInlineStyle(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleAllowInlineWithDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleDisallowInline(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->allowInlineStyle(false); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyImageDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: www.nextcloud.com;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyImageDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: www.nextcloud.com www.nextcloud.org;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowImageDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowImageDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: www.nextcloud.com;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowImageDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org')->disallowImageDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFontDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data: www.nextcloud.com;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFontDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data: www.nextcloud.com www.nextcloud.org;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFontDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFontDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data: www.nextcloud.com;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFontDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org')->disallowFontDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyConnectDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self' www.nextcloud.com;media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyConnectDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self' www.nextcloud.com www.nextcloud.org;media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowConnectDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowConnectDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self' www.nextcloud.com;media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowConnectDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org')->disallowConnectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyMediaDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self' www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyMediaDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self' www.nextcloud.com www.nextcloud.org;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowMediaDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowMediaDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self' www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowMediaDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org')->disallowMediaDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyObjectDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';object-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyObjectDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';object-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowObjectDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowObjectDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';object-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowObjectDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org')->disallowObjectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetAllowedFrameDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFrameDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org')->disallowFrameDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetAllowedChildSrcDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';child-src child.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('child.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyChildSrcValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';child-src child.nextcloud.com child.nextcloud.org;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('child.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedChildSrcDomain('child.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowChildSrcDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowChildSrcDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';child-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowChildSrcDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.org')->disallowChildSrcDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + + + public function testGetAllowedFrameAncestorDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self' sub.nextcloud.com;form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('sub.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFrameAncestorValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self' sub.nextcloud.com foo.nextcloud.com;form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('sub.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('foo.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameAncestorDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameAncestorDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameAncestorDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self' www.nextcloud.com;form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameAncestorDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameAncestorDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.org')->disallowChildSrcDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyUnsafeEval(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->allowEvalScript(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyUnsafeWasmEval(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'wasm-unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->allowEvalWasm(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyNonce(): void { + $nonce = base64_encode('my-nonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->contentSecurityPolicy->useStrictDynamicOnScripts(false); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyNonceDefault(): void { + $nonce = base64_encode('my-nonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce';script-src-elem 'strict-dynamic' 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyNonceStrictDynamic(): void { + $nonce = base64_encode('my-nonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->contentSecurityPolicy->useStrictDynamic(true); + $this->contentSecurityPolicy->useStrictDynamicOnScripts(false); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyNonceStrictDynamicDefault(): void { + $nonce = base64_encode('my-nonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->contentSecurityPolicy->useStrictDynamic(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStrictDynamicOnScriptsOff(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->useStrictDynamicOnScripts(false); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStrictDynamicAndStrictDynamicOnScripts(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; + + $this->contentSecurityPolicy->useStrictDynamic(true); + $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } +} diff --git a/tests/lib/AppFramework/Http/DataResponseTest.php b/tests/lib/AppFramework/Http/DataResponseTest.php new file mode 100644 index 00000000000..e9a2c511140 --- /dev/null +++ b/tests/lib/AppFramework/Http/DataResponseTest.php @@ -0,0 +1,75 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IRequest; +use OCP\Server; + +class DataResponseTest extends \Test\TestCase { + /** + * @var DataResponse + */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->response = new DataResponse(); + } + + + public function testSetData(): void { + $params = ['hi', 'yo']; + $this->response->setData($params); + + $this->assertEquals(['hi', 'yo'], $this->response->getData()); + } + + + public function testConstructorAllowsToSetData(): void { + $data = ['hi']; + $code = 300; + $response = new DataResponse($data, $code); + + $this->assertEquals($data, $response->getData()); + $this->assertEquals($code, $response->getStatus()); + } + + + public function testConstructorAllowsToSetHeaders(): void { + $data = ['hi']; + $code = 300; + $headers = ['test' => 'something']; + $response = new DataResponse($data, $code, $headers); + + $expectedHeaders = [ + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'", + 'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'", + 'X-Robots-Tag' => 'noindex, nofollow', + 'X-Request-Id' => Server::get(IRequest::class)->getId(), + ]; + $expectedHeaders = array_merge($expectedHeaders, $headers); + + $this->assertEquals($data, $response->getData()); + $this->assertEquals($code, $response->getStatus()); + $this->assertEquals($expectedHeaders, $response->getHeaders()); + } + + + public function testChainability(): void { + $params = ['hi', 'yo']; + $this->response->setData($params) + ->setStatus(Http::STATUS_NOT_FOUND); + + $this->assertEquals(Http::STATUS_NOT_FOUND, $this->response->getStatus()); + $this->assertEquals(['hi', 'yo'], $this->response->getData()); + } +} diff --git a/tests/lib/AppFramework/Http/DispatcherTest.php b/tests/lib/AppFramework/Http/DispatcherTest.php new file mode 100644 index 00000000000..86c78e840e0 --- /dev/null +++ b/tests/lib/AppFramework/Http/DispatcherTest.php @@ -0,0 +1,582 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OC\AppFramework\DependencyInjection\DIContainer; +use OC\AppFramework\Http\Dispatcher; +use OC\AppFramework\Http\Request; +use OC\AppFramework\Middleware\MiddlewareDispatcher; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\ParameterOutOfRangeException; +use OCP\AppFramework\Http\Response; +use OCP\Diagnostics\IEventLogger; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IRequest; +use OCP\IRequestId; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +class TestController extends Controller { + /** + * @param string $appName + * @param IRequest $request + */ + public function __construct($appName, $request) { + parent::__construct($appName, $request); + } + + /** + * @param int $int + * @param bool $bool + * @param double $foo + * @param int $test + * @param integer $test2 + * @return array + */ + public function exec($int, $bool, $foo, $test = 4, $test2 = 1) { + $this->registerResponder('text', function ($in) { + return new JSONResponse(['text' => $in]); + }); + return [$int, $bool, $test, $test2]; + } + + + /** + * @param int $int + * @param bool $bool + * @param int $test + * @param int $test2 + * @return DataResponse + */ + public function execDataResponse($int, $bool, $test = 4, $test2 = 1) { + return new DataResponse([ + 'text' => [$int, $bool, $test, $test2] + ]); + } + + public function test(): Response { + return new DataResponse(); + } +} + +/** + * Class DispatcherTest + * + * @package Test\AppFramework\Http + * @group DB + */ +class DispatcherTest extends \Test\TestCase { + /** @var MiddlewareDispatcher */ + private $middlewareDispatcher; + /** @var Dispatcher */ + private $dispatcher; + private $controllerMethod; + /** @var Controller|MockObject */ + private $controller; + private $response; + /** @var IRequest|MockObject */ + private $request; + private $lastModified; + private $etag; + /** @var Http|MockObject */ + private $http; + private $reflector; + /** @var IConfig|MockObject */ + private $config; + /** @var LoggerInterface|MockObject */ + private $logger; + /** @var IEventLogger|MockObject */ + private $eventLogger; + /** @var ContainerInterface|MockObject */ + private $container; + + protected function setUp(): void { + parent::setUp(); + $this->controllerMethod = 'test'; + + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->eventLogger = $this->createMock(IEventLogger::class); + $this->container = $this->createMock(ContainerInterface::class); + $app = $this->createMock(DIContainer::class); + $request = $this->createMock(Request::class); + $this->http = $this->createMock(\OC\AppFramework\Http::class); + + $this->middlewareDispatcher = $this->createMock(MiddlewareDispatcher::class); + $this->controller = $this->getMockBuilder(TestController::class) + ->onlyMethods([$this->controllerMethod]) + ->setConstructorArgs([$app, $request]) + ->getMock(); + + $this->request = $this->createMock(Request::class); + + $this->reflector = new ControllerMethodReflector(); + + $this->dispatcher = new Dispatcher( + $this->http, + $this->middlewareDispatcher, + $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container, + ); + + $this->response = $this->createMock(Response::class); + + $this->lastModified = new \DateTime('now', new \DateTimeZone('GMT')); + $this->etag = 'hi'; + } + + + /** + * @param string $out + * @param string $httpHeaders + */ + private function setMiddlewareExpectations($out = null, + $httpHeaders = null, $responseHeaders = [], + $ex = false, $catchEx = true) { + if ($ex) { + $exception = new \Exception(); + $this->middlewareDispatcher->expects($this->once()) + ->method('beforeController') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)) + ->willThrowException($exception); + if ($catchEx) { + $this->middlewareDispatcher->expects($this->once()) + ->method('afterException') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod), + $this->equalTo($exception)) + ->willReturn($this->response); + } else { + $this->middlewareDispatcher->expects($this->once()) + ->method('afterException') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod), + $this->equalTo($exception)) + ->willThrowException($exception); + return; + } + } else { + $this->middlewareDispatcher->expects($this->once()) + ->method('beforeController') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod)); + $this->controller->expects($this->once()) + ->method($this->controllerMethod) + ->willReturn($this->response); + } + + $this->response->expects($this->once()) + ->method('render') + ->willReturn($out); + $this->response->expects($this->once()) + ->method('getStatus') + ->willReturn(Http::STATUS_OK); + $this->response->expects($this->once()) + ->method('getHeaders') + ->willReturn($responseHeaders); + $this->http->expects($this->once()) + ->method('getStatusHeader') + ->with($this->equalTo(Http::STATUS_OK)) + ->willReturn($httpHeaders); + + $this->middlewareDispatcher->expects($this->once()) + ->method('afterController') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod), + $this->equalTo($this->response)) + ->willReturn($this->response); + + $this->middlewareDispatcher->expects($this->once()) + ->method('afterController') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod), + $this->equalTo($this->response)) + ->willReturn($this->response); + + $this->middlewareDispatcher->expects($this->once()) + ->method('beforeOutput') + ->with($this->equalTo($this->controller), + $this->equalTo($this->controllerMethod), + $this->equalTo($out)) + ->willReturn($out); + } + + + public function testDispatcherReturnsArrayWith2Entries(): void { + $this->setMiddlewareExpectations(''); + + $response = $this->dispatcher->dispatch($this->controller, $this->controllerMethod); + $this->assertNull($response[0]); + $this->assertEquals([], $response[1]); + $this->assertNull($response[2]); + } + + + public function testHeadersAndOutputAreReturned(): void { + $out = 'yo'; + $httpHeaders = 'Http'; + $responseHeaders = ['hell' => 'yeah']; + $this->setMiddlewareExpectations($out, $httpHeaders, $responseHeaders); + + $response = $this->dispatcher->dispatch($this->controller, + $this->controllerMethod); + + $this->assertEquals($httpHeaders, $response[0]); + $this->assertEquals($responseHeaders, $response[1]); + $this->assertEquals($out, $response[3]); + } + + + public function testExceptionCallsAfterException(): void { + $out = 'yo'; + $httpHeaders = 'Http'; + $responseHeaders = ['hell' => 'yeah']; + $this->setMiddlewareExpectations($out, $httpHeaders, $responseHeaders, true); + + $response = $this->dispatcher->dispatch($this->controller, + $this->controllerMethod); + + $this->assertEquals($httpHeaders, $response[0]); + $this->assertEquals($responseHeaders, $response[1]); + $this->assertEquals($out, $response[3]); + } + + + public function testExceptionThrowsIfCanNotBeHandledByAfterException(): void { + $out = 'yo'; + $httpHeaders = 'Http'; + $responseHeaders = ['hell' => 'yeah']; + $this->setMiddlewareExpectations($out, $httpHeaders, $responseHeaders, true, false); + + $this->expectException(\Exception::class); + $this->dispatcher->dispatch( + $this->controller, + $this->controllerMethod + ); + } + + + private function dispatcherPassthrough() { + $this->middlewareDispatcher->expects($this->once()) + ->method('beforeController'); + $this->middlewareDispatcher->expects($this->once()) + ->method('afterController') + ->willReturnCallback(function ($a, $b, $in) { + return $in; + }); + $this->middlewareDispatcher->expects($this->once()) + ->method('beforeOutput') + ->willReturnCallback(function ($a, $b, $in) { + return $in; + }); + } + + + public function testControllerParametersInjected(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + ], + 'method' => 'POST' + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'exec'); + + $this->assertEquals('[3,false,4,1]', $response[3]); + } + + + public function testControllerParametersInjectedDefaultOverwritten(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + 'test2' => 7 + ], + 'method' => 'POST', + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'exec'); + + $this->assertEquals('[3,false,4,7]', $response[3]); + } + + + + public function testResponseTransformedByUrlFormat(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + ], + 'urlParams' => [ + 'format' => 'text' + ], + 'method' => 'GET' + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'exec'); + + $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); + } + + + public function testResponseTransformsDataResponse(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + ], + 'urlParams' => [ + 'format' => 'json' + ], + 'method' => 'GET' + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'execDataResponse'); + + $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); + } + + + public function testResponseTransformedByAcceptHeader(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + ], + 'server' => [ + 'HTTP_ACCEPT' => 'application/text, test', + 'HTTP_CONTENT_TYPE' => 'application/x-www-form-urlencoded' + ], + 'method' => 'PUT' + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'exec'); + + $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); + } + + public function testResponseTransformedBySendingMultipartFormData(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + ], + 'server' => [ + 'HTTP_ACCEPT' => 'application/text, test', + 'HTTP_CONTENT_TYPE' => 'multipart/form-data' + ], + 'method' => 'POST' + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'exec'); + + $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); + } + + + public function testResponsePrimarilyTransformedByParameterFormat(): void { + $this->request = new Request( + [ + 'post' => [ + 'int' => '3', + 'bool' => 'false', + 'double' => 1.2, + ], + 'get' => [ + 'format' => 'text' + ], + 'server' => [ + 'HTTP_ACCEPT' => 'application/json, test' + ], + 'method' => 'POST' + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->dispatcher = new Dispatcher( + $this->http, $this->middlewareDispatcher, $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container + ); + $controller = new TestController('app', $this->request); + + // reflector is supposed to be called once + $this->dispatcherPassthrough(); + $response = $this->dispatcher->dispatch($controller, 'exec'); + + $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); + } + + + public static function rangeDataProvider(): array { + return [ + [PHP_INT_MIN, PHP_INT_MAX, 42, false], + [0, 12, -5, true], + [-12, 0, 5, true], + [7, 14, 5, true], + [7, 14, 10, false], + [-14, -7, -10, false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('rangeDataProvider')] + public function testEnsureParameterValueSatisfiesRange(int $min, int $max, int $input, bool $throw): void { + $this->reflector = $this->createMock(ControllerMethodReflector::class); + $this->reflector->expects($this->any()) + ->method('getRange') + ->willReturn([ + 'min' => $min, + 'max' => $max, + ]); + + $this->dispatcher = new Dispatcher( + $this->http, + $this->middlewareDispatcher, + $this->reflector, + $this->request, + $this->config, + Server::get(IDBConnection::class), + $this->logger, + $this->eventLogger, + $this->container, + ); + + if ($throw) { + $this->expectException(ParameterOutOfRangeException::class); + } + + $this->invokePrivate($this->dispatcher, 'ensureParameterValueSatisfiesRange', ['myArgument', $input]); + if (!$throw) { + // do not mark this test risky + $this->assertTrue(true); + } + } +} diff --git a/tests/lib/AppFramework/Http/DownloadResponseTest.php b/tests/lib/AppFramework/Http/DownloadResponseTest.php new file mode 100644 index 00000000000..b2f60edd999 --- /dev/null +++ b/tests/lib/AppFramework/Http/DownloadResponseTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http\DownloadResponse; + +class ChildDownloadResponse extends DownloadResponse { +}; + + +class DownloadResponseTest extends \Test\TestCase { + protected function setUp(): void { + parent::setUp(); + } + + public function testHeaders(): void { + $response = new ChildDownloadResponse('file', 'content'); + $headers = $response->getHeaders(); + + $this->assertEquals('attachment; filename="file"', $headers['Content-Disposition']); + $this->assertEquals('content', $headers['Content-Type']); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('filenameEncodingProvider')] + public function testFilenameEncoding(string $input, string $expected): void { + $response = new ChildDownloadResponse($input, 'content'); + $headers = $response->getHeaders(); + + $this->assertEquals('attachment; filename="' . $expected . '"', $headers['Content-Disposition']); + } + + public static function filenameEncodingProvider() : array { + return [ + ['TestName.txt', 'TestName.txt'], + ['A "Quoted" Filename.txt', 'A \\"Quoted\\" Filename.txt'], + ['A "Quoted" Filename.txt', 'A \\"Quoted\\" Filename.txt'], + ['A "Quoted" Filename With A Backslash \\.txt', 'A \\"Quoted\\" Filename With A Backslash \\\\.txt'], + ['A "Very" Weird Filename \ / & <> " >\'""""\.text', 'A \\"Very\\" Weird Filename \\\\ / & <> \\" >\'\\"\\"\\"\\"\\\\.text'], + ['\\\\\\\\\\\\', '\\\\\\\\\\\\\\\\\\\\\\\\'], + ]; + } +} diff --git a/tests/lib/AppFramework/Http/EmptyContentSecurityPolicyTest.php b/tests/lib/AppFramework/Http/EmptyContentSecurityPolicyTest.php new file mode 100644 index 00000000000..66abce43cc4 --- /dev/null +++ b/tests/lib/AppFramework/Http/EmptyContentSecurityPolicyTest.php @@ -0,0 +1,498 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; + +/** + * Class ContentSecurityPolicyTest + * + * @package OC\AppFramework\Http + */ +class EmptyContentSecurityPolicyTest extends \Test\TestCase { + /** @var EmptyContentSecurityPolicy */ + private $contentSecurityPolicy; + + protected function setUp(): void { + parent::setUp(); + $this->contentSecurityPolicy = new EmptyContentSecurityPolicy(); + } + + public function testGetPolicyDefault(): void { + $defaultPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + $this->assertSame($defaultPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowScriptDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowScriptDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowScriptDomainMultipleStacked(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org')->disallowScriptDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptAllowEval(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'unsafe-eval';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->allowEvalScript(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyScriptAllowWasmEval(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'wasm-unsafe-eval';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->allowEvalWasm(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowStyleDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowStyleDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowStyleDomainMultipleStacked(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org')->disallowStyleDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleAllowInline(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src 'unsafe-inline';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->allowInlineStyle(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleAllowInlineWithDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com 'unsafe-inline';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->allowInlineStyle(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyStyleDisallowInline(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->allowInlineStyle(false); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyImageDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';img-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyImageDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';img-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowImageDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowImageDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';img-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowImageDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org')->disallowImageDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFontDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';font-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFontDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';font-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFontDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFontDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';font-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFontDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org')->disallowFontDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyConnectDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';connect-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyConnectDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';connect-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowConnectDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowConnectDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';connect-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowConnectDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org')->disallowConnectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyMediaDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';media-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyMediaDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';media-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowMediaDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowMediaDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';media-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowMediaDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org')->disallowMediaDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyObjectDomainValid(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';object-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyObjectDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';object-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowObjectDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowObjectDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';object-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowObjectDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org')->disallowObjectDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetAllowedFrameDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyFrameDomainValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowFrameDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org')->disallowFrameDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetAllowedChildSrcDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';child-src child.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('child.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyChildSrcValidMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';child-src child.nextcloud.com child.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('child.nextcloud.com'); + $this->contentSecurityPolicy->addAllowedChildSrcDomain('child.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowChildSrcDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowChildSrcDomainMultiple(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';child-src www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyDisallowChildSrcDomainMultipleStakes(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedChildSrcDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->disallowChildSrcDomain('www.nextcloud.org')->disallowChildSrcDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithJsNonceAndScriptDomains(): void { + $nonce = base64_encode('MyJsNonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce' www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithJsNonceAndStrictDynamic(): void { + $nonce = base64_encode('MyJsNonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce' www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->useStrictDynamic(true); + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithJsNonceAndStrictDynamicAndStrictDynamicOnScripts(): void { + $nonce = base64_encode('MyJsNonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce' www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->useStrictDynamic(true); + $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); + $this->contentSecurityPolicy->useJsNonce($nonce); + // Should be same as `testGetPolicyWithJsNonceAndStrictDynamic` because of fallback + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithJsNonceAndStrictDynamicOnScripts(): void { + $nonce = base64_encode('MyJsNonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce' www.nextcloud.com;script-src-elem 'strict-dynamic' 'nonce-$nonce' www.nextcloud.com;frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); + $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithStrictDynamicOnScripts(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithJsNonceAndSelfScriptDomain(): void { + $nonce = base64_encode('MyJsNonce'); + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->useJsNonce($nonce); + $this->contentSecurityPolicy->addAllowedScriptDomain("'self'"); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithoutJsNonceAndSelfScriptDomain(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';frame-ancestors 'none'"; + + $this->contentSecurityPolicy->addAllowedScriptDomain("'self'"); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithReportUri(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none';report-uri https://my-report-uri.com"; + + $this->contentSecurityPolicy->addReportTo('https://my-report-uri.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } + + public function testGetPolicyWithMultipleReportUri(): void { + $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none';report-uri https://my-report-uri.com https://my-other-report-uri.com"; + + $this->contentSecurityPolicy->addReportTo('https://my-report-uri.com'); + $this->contentSecurityPolicy->addReportTo('https://my-other-report-uri.com'); + $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); + } +} diff --git a/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php b/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php new file mode 100644 index 00000000000..71342485552 --- /dev/null +++ b/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http\EmptyFeaturePolicy; + +class EmptyFeaturePolicyTest extends \Test\TestCase { + /** @var EmptyFeaturePolicy */ + private $policy; + + protected function setUp(): void { + parent::setUp(); + $this->policy = new EmptyFeaturePolicy(); + } + + public function testGetPolicyDefault(): void { + $defaultPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + $this->assertSame($defaultPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValid(): void { + $expectedPolicy = "autoplay www.nextcloud.com;camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValidMultiple(): void { + $expectedPolicy = "autoplay www.nextcloud.com www.nextcloud.org;camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->policy->addAllowedAutoplayDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValid(): void { + $expectedPolicy = "autoplay 'none';camera www.nextcloud.com;fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'none';camera www.nextcloud.com www.nextcloud.org;fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->policy->addAllowedCameraDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValid(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen www.nextcloud.com;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen www.nextcloud.com www.nextcloud.org;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->policy->addAllowedFullScreenDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValid(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation www.nextcloud.com;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation www.nextcloud.com www.nextcloud.org;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValid(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone www.nextcloud.com;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone www.nextcloud.com www.nextcloud.org;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValid(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment www.nextcloud.com"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment www.nextcloud.com www.nextcloud.org"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->policy->addAllowedPaymentDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } +} diff --git a/tests/lib/AppFramework/Http/FeaturePolicyTest.php b/tests/lib/AppFramework/Http/FeaturePolicyTest.php new file mode 100644 index 00000000000..6ea990fb111 --- /dev/null +++ b/tests/lib/AppFramework/Http/FeaturePolicyTest.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http\FeaturePolicy; + +class FeaturePolicyTest extends \Test\TestCase { + /** @var EmptyFeaturePolicy */ + private $policy; + + protected function setUp(): void { + parent::setUp(); + $this->policy = new FeaturePolicy(); + } + + public function testGetPolicyDefault(): void { + $defaultPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + $this->assertSame($defaultPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValid(): void { + $expectedPolicy = "autoplay 'self' www.nextcloud.com;camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyAutoplayDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'self' www.nextcloud.com www.nextcloud.org;camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); + $this->policy->addAllowedAutoplayDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValid(): void { + $expectedPolicy = "autoplay 'self';camera www.nextcloud.com;fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyCameraDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'self';camera www.nextcloud.com www.nextcloud.org;fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedCameraDomain('www.nextcloud.com'); + $this->policy->addAllowedCameraDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValid(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self' www.nextcloud.com;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyFullScreenDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self' www.nextcloud.com www.nextcloud.org;geolocation 'none';microphone 'none';payment 'none'"; + + $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); + $this->policy->addAllowedFullScreenDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValid(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation www.nextcloud.com;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyGeoLocationDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation www.nextcloud.com www.nextcloud.org;microphone 'none';payment 'none'"; + + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); + $this->policy->addAllowedGeoLocationDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValid(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone www.nextcloud.com;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyMicrophoneDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone www.nextcloud.com www.nextcloud.org;payment 'none'"; + + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); + $this->policy->addAllowedMicrophoneDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValid(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment www.nextcloud.com"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } + + public function testGetPolicyPaymentDomainValidMultiple(): void { + $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment www.nextcloud.com www.nextcloud.org"; + + $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); + $this->policy->addAllowedPaymentDomain('www.nextcloud.org'); + $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); + } +} diff --git a/tests/lib/AppFramework/Http/FileDisplayResponseTest.php b/tests/lib/AppFramework/Http/FileDisplayResponseTest.php new file mode 100644 index 00000000000..029ddaad712 --- /dev/null +++ b/tests/lib/AppFramework/Http/FileDisplayResponseTest.php @@ -0,0 +1,94 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\Files\File; + +class FileDisplayResponseTest extends \Test\TestCase { + /** @var File|\PHPUnit\Framework\MockObject\MockObject */ + private $file; + + /** @var FileDisplayResponse */ + private $response; + + protected function setUp(): void { + $this->file = $this->getMockBuilder('OCP\Files\File') + ->getMock(); + + $this->file->expects($this->once()) + ->method('getETag') + ->willReturn('myETag'); + $this->file->expects($this->once()) + ->method('getName') + ->willReturn('myFileName'); + $this->file->expects($this->once()) + ->method('getMTime') + ->willReturn(1464825600); + + $this->response = new FileDisplayResponse($this->file); + } + + public function testHeader(): void { + $headers = $this->response->getHeaders(); + $this->assertArrayHasKey('Content-Disposition', $headers); + $this->assertSame('inline; filename="myFileName"', $headers['Content-Disposition']); + } + + public function testETag(): void { + $this->assertSame('myETag', $this->response->getETag()); + } + + public function testLastModified(): void { + $lastModified = $this->response->getLastModified(); + $this->assertNotNull($lastModified); + $this->assertSame(1464825600, $lastModified->getTimestamp()); + } + + public function test304(): void { + $output = $this->getMockBuilder('OCP\AppFramework\Http\IOutput') + ->disableOriginalConstructor() + ->getMock(); + + $output->expects($this->any()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_NOT_MODIFIED); + $output->expects($this->never()) + ->method('setOutput'); + $this->file->expects($this->never()) + ->method('getContent'); + + $this->response->callback($output); + } + + + public function testNon304(): void { + $output = $this->getMockBuilder('OCP\AppFramework\Http\IOutput') + ->disableOriginalConstructor() + ->getMock(); + + $output->expects($this->any()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_OK); + $output->expects($this->once()) + ->method('setOutput') + ->with($this->equalTo('my data')); + $output->expects($this->once()) + ->method('setHeader') + ->with($this->equalTo('Content-Length: 42')); + $this->file->expects($this->once()) + ->method('getContent') + ->willReturn('my data'); + $this->file->expects($this->any()) + ->method('getSize') + ->willReturn(42); + + $this->response->callback($output); + } +} diff --git a/tests/lib/AppFramework/Http/HttpTest.php b/tests/lib/AppFramework/Http/HttpTest.php new file mode 100644 index 00000000000..d3ec8438554 --- /dev/null +++ b/tests/lib/AppFramework/Http/HttpTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OC\AppFramework\Http; + +class HttpTest extends \Test\TestCase { + private $server; + + /** + * @var Http + */ + private $http; + + protected function setUp(): void { + parent::setUp(); + + $this->server = []; + $this->http = new Http($this->server); + } + + + public function testProtocol(): void { + $header = $this->http->getStatusHeader(Http::STATUS_TEMPORARY_REDIRECT); + $this->assertEquals('HTTP/1.1 307 Temporary Redirect', $header); + } + + + public function testProtocol10(): void { + $this->http = new Http($this->server, 'HTTP/1.0'); + $header = $this->http->getStatusHeader(Http::STATUS_OK); + $this->assertEquals('HTTP/1.0 200 OK', $header); + } + + public function testTempRedirectBecomesFoundInHttp10(): void { + $http = new Http([], 'HTTP/1.0'); + + $header = $http->getStatusHeader(Http::STATUS_TEMPORARY_REDIRECT); + $this->assertEquals('HTTP/1.0 302 Found', $header); + } + // TODO: write unittests for http codes +} diff --git a/tests/lib/AppFramework/Http/JSONResponseTest.php b/tests/lib/AppFramework/Http/JSONResponseTest.php new file mode 100644 index 00000000000..56f67b23f0d --- /dev/null +++ b/tests/lib/AppFramework/Http/JSONResponseTest.php @@ -0,0 +1,98 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; + +class JSONResponseTest extends \Test\TestCase { + /** + * @var JSONResponse + */ + private $json; + + protected function setUp(): void { + parent::setUp(); + $this->json = new JSONResponse(); + } + + + public function testHeader(): void { + $headers = $this->json->getHeaders(); + $this->assertEquals('application/json; charset=utf-8', $headers['Content-Type']); + } + + + public function testSetData(): void { + $params = ['hi', 'yo']; + $this->json->setData($params); + + $this->assertEquals(['hi', 'yo'], $this->json->getData()); + } + + + public function testSetRender(): void { + $params = ['test' => 'hi']; + $this->json->setData($params); + + $expected = '{"test":"hi"}'; + + $this->assertEquals($expected, $this->json->render()); + } + + public static function renderDataProvider(): array { + return [ + [ + ['test' => 'hi'], '{"test":"hi"}', + ], + [ + ['<h1>test' => '<h1>hi'], '{"\u003Ch1\u003Etest":"\u003Ch1\u003Ehi"}', + ], + ]; + } + + /** + * @param array $input + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('renderDataProvider')] + public function testRender(array $input, $expected): void { + $this->json->setData($input); + $this->assertEquals($expected, $this->json->render()); + } + + + public function testRenderWithNonUtf8Encoding(): void { + $this->expectException(\JsonException::class); + $this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded'); + + $params = ['test' => hex2bin('e9')]; + $this->json->setData($params); + $this->json->render(); + } + + public function testConstructorAllowsToSetData(): void { + $data = ['hi']; + $code = 300; + $response = new JSONResponse($data, $code); + + $expected = '["hi"]'; + $this->assertEquals($expected, $response->render()); + $this->assertEquals($code, $response->getStatus()); + } + + public function testChainability(): void { + $params = ['hi', 'yo']; + $this->json->setData($params) + ->setStatus(Http::STATUS_NOT_FOUND); + + $this->assertEquals(Http::STATUS_NOT_FOUND, $this->json->getStatus()); + $this->assertEquals(['hi', 'yo'], $this->json->getData()); + } +} diff --git a/tests/lib/AppFramework/Http/OutputTest.php b/tests/lib/AppFramework/Http/OutputTest.php new file mode 100644 index 00000000000..2ba93833dd1 --- /dev/null +++ b/tests/lib/AppFramework/Http/OutputTest.php @@ -0,0 +1,30 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OC\AppFramework\Http\Output; + +class OutputTest extends \Test\TestCase { + public function testSetOutput(): void { + $this->expectOutputString('foo'); + $output = new Output(''); + $output->setOutput('foo'); + } + + public function testSetReadfile(): void { + $this->expectOutputString(file_get_contents(__FILE__)); + $output = new Output(''); + $output->setReadfile(__FILE__); + } + + public function testSetReadfileStream(): void { + $this->expectOutputString(file_get_contents(__FILE__)); + $output = new Output(''); + $output->setReadfile(fopen(__FILE__, 'r')); + } +} diff --git a/tests/lib/AppFramework/Http/PublicTemplateResponseTest.php b/tests/lib/AppFramework/Http/PublicTemplateResponseTest.php new file mode 100644 index 00000000000..cb7bd97f5da --- /dev/null +++ b/tests/lib/AppFramework/Http/PublicTemplateResponseTest.php @@ -0,0 +1,62 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http\Template\PublicTemplateResponse; +use OCP\AppFramework\Http\Template\SimpleMenuAction; +use Test\TestCase; + +class PublicTemplateResponseTest extends TestCase { + public function testSetParamsConstructor(): void { + $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); + $this->assertEquals(['key' => 'value'], $template->getParams()); + } + + public function testAdditionalElements(): void { + $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); + $template->setHeaderTitle('Header'); + $template->setHeaderDetails('Details'); + $this->assertEquals(['key' => 'value'], $template->getParams()); + $this->assertEquals('Header', $template->getHeaderTitle()); + $this->assertEquals('Details', $template->getHeaderDetails()); + } + + public function testActionSingle(): void { + $actions = [ + new SimpleMenuAction('link', 'Download', 'download', 'downloadLink', 0) + ]; + $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); + $template->setHeaderActions($actions); + $this->assertEquals(['key' => 'value'], $template->getParams()); + $this->assertEquals($actions[0], $template->getPrimaryAction()); + $this->assertEquals(1, $template->getActionCount()); + $this->assertEquals([], $template->getOtherActions()); + } + + + public function testActionMultiple(): void { + $actions = [ + new SimpleMenuAction('link1', 'Download1', 'download1', 'downloadLink1', 100), + new SimpleMenuAction('link2', 'Download2', 'download2', 'downloadLink2', 20), + new SimpleMenuAction('link3', 'Download3', 'download3', 'downloadLink3', 0) + ]; + $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); + $template->setHeaderActions($actions); + $this->assertEquals(['key' => 'value'], $template->getParams()); + $this->assertEquals($actions[2], $template->getPrimaryAction()); + $this->assertEquals(3, $template->getActionCount()); + $this->assertEquals([$actions[1], $actions[0]], $template->getOtherActions()); + } + + + public function testGetRenderAs(): void { + $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); + $this->assertEquals(['key' => 'value'], $template->getParams()); + $this->assertEquals('public', $template->getRenderAs()); + } +} diff --git a/tests/lib/AppFramework/Http/RedirectResponseTest.php b/tests/lib/AppFramework/Http/RedirectResponseTest.php new file mode 100644 index 00000000000..f6319782e79 --- /dev/null +++ b/tests/lib/AppFramework/Http/RedirectResponseTest.php @@ -0,0 +1,37 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\RedirectResponse; + +class RedirectResponseTest extends \Test\TestCase { + /** + * @var RedirectResponse + */ + protected $response; + + protected function setUp(): void { + parent::setUp(); + $this->response = new RedirectResponse('/url'); + } + + + public function testHeaders(): void { + $headers = $this->response->getHeaders(); + $this->assertEquals('/url', $headers['Location']); + $this->assertEquals(Http::STATUS_SEE_OTHER, + $this->response->getStatus()); + } + + + public function testGetRedirectUrl(): void { + $this->assertEquals('/url', $this->response->getRedirectUrl()); + } +} diff --git a/tests/lib/AppFramework/Http/RequestIdTest.php b/tests/lib/AppFramework/Http/RequestIdTest.php new file mode 100644 index 00000000000..9cfd3b1785c --- /dev/null +++ b/tests/lib/AppFramework/Http/RequestIdTest.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OC\AppFramework\Http\RequestId; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Class RequestIdTest + * + * @package OC\AppFramework\Http + */ +class RequestIdTest extends \Test\TestCase { + /** @var ISecureRandom|MockObject */ + protected $secureRandom; + + protected function setUp(): void { + parent::setUp(); + + $this->secureRandom = $this->createMock(ISecureRandom::class); + } + + public function testGetIdWithModUnique(): void { + $requestId = new RequestId( + 'GeneratedUniqueIdByModUnique', + $this->secureRandom + ); + + $this->secureRandom->expects($this->never()) + ->method('generate'); + + $this->assertSame('GeneratedUniqueIdByModUnique', $requestId->getId()); + $this->assertSame('GeneratedUniqueIdByModUnique', $requestId->getId()); + } + + public function testGetIdWithoutModUnique(): void { + $requestId = new RequestId( + '', + $this->secureRandom + ); + + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with('20') + ->willReturn('GeneratedByNextcloudItself1'); + + $this->assertSame('GeneratedByNextcloudItself1', $requestId->getId()); + $this->assertSame('GeneratedByNextcloudItself1', $requestId->getId()); + } +} diff --git a/tests/lib/AppFramework/Http/RequestStream.php b/tests/lib/AppFramework/Http/RequestStream.php new file mode 100644 index 00000000000..7340391b2d5 --- /dev/null +++ b/tests/lib/AppFramework/Http/RequestStream.php @@ -0,0 +1,116 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\AppFramework\Http; + +/** + * Copy of http://dk1.php.net/manual/en/stream.streamwrapper.example-1.php + * Used to simulate php://input for Request tests + */ +class RequestStream { + protected int $position = 0; + protected string $varname = ''; + /* @var resource */ + public $context; + + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool { + $url = parse_url($path); + $this->varname = $url['host'] ?? ''; + $this->position = 0; + + return true; + } + + public function stream_read(int $count): string { + $ret = substr($GLOBALS[$this->varname], $this->position, $count); + $this->position += strlen($ret); + return $ret; + } + + public function stream_write(string $data): int { + $left = substr($GLOBALS[$this->varname], 0, $this->position); + $right = substr($GLOBALS[$this->varname], $this->position + strlen($data)); + $GLOBALS[$this->varname] = $left . $data . $right; + $this->position += strlen($data); + return strlen($data); + } + + public function stream_tell(): int { + return $this->position; + } + + public function stream_eof(): bool { + return $this->position >= strlen($GLOBALS[$this->varname]); + } + + public function stream_seek(int $offset, int $whence = SEEK_SET): bool { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($GLOBALS[$this->varname]) && $offset >= 0) { + $this->position = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen($GLOBALS[$this->varname]) + $offset >= 0) { + $this->position = strlen($GLOBALS[$this->varname]) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } + + public function stream_stat(): array { + $size = strlen($GLOBALS[$this->varname]); + $time = time(); + $data = [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0777, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => '', + 'size' => $size, + 'atime' => $time, + 'mtime' => $time, + 'ctime' => $time, + 'blksize' => -1, + 'blocks' => -1, + ]; + return array_values($data) + $data; + //return false; + } + + public function stream_metadata(string $path, int $option, $var): bool { + if ($option == STREAM_META_TOUCH) { + $url = parse_url($path); + $varname = $url['host'] ?? ''; + if (!isset($GLOBALS[$varname])) { + $GLOBALS[$varname] = ''; + } + return true; + } + return false; + } +} diff --git a/tests/lib/AppFramework/Http/RequestTest.php b/tests/lib/AppFramework/Http/RequestTest.php new file mode 100644 index 00000000000..7ea2cb31482 --- /dev/null +++ b/tests/lib/AppFramework/Http/RequestTest.php @@ -0,0 +1,2262 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\AppFramework\Http; + +use OC\AppFramework\Http\Request; +use OC\Security\CSRF\CsrfToken; +use OC\Security\CSRF\CsrfTokenManager; +use OCP\IConfig; +use OCP\IRequestId; + +/** + * Class RequestTest + * + * @package OC\AppFramework\Http + */ +class RequestTest extends \Test\TestCase { + /** @var string */ + protected $stream = 'fakeinput://data'; + /** @var IRequestId */ + protected $requestId; + /** @var IConfig */ + protected $config; + /** @var CsrfTokenManager */ + protected $csrfTokenManager; + + protected function setUp(): void { + parent::setUp(); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'Test\AppFramework\Http\RequestStream'); + + $this->requestId = $this->createMock(IRequestId::class); + $this->config = $this->createMock(IConfig::class); + $this->csrfTokenManager = $this->getMockBuilder(CsrfTokenManager::class) + ->disableOriginalConstructor() + ->getMock(); + } + + protected function tearDown(): void { + stream_wrapper_unregister('fakeinput'); + parent::tearDown(); + } + + public function testRequestAccessors(): void { + $vars = [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'method' => 'GET', + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + // Countable + $this->assertSame(2, count($request)); + // Array access + $this->assertSame('Joey', $request['nickname']); + // "Magic" accessors + $this->assertSame('Joey', $request->{'nickname'}); + $this->assertTrue(isset($request['nickname'])); + $this->assertTrue(isset($request->{'nickname'})); + $this->assertFalse(isset($request->{'flickname'})); + // Only testing 'get', but same approach for post, files etc. + $this->assertSame('Joey', $request->get['nickname']); + // Always returns null if variable not set. + $this->assertSame(null, $request->{'flickname'}); + } + + // urlParams has precedence over POST which has precedence over GET + public function testPrecedence(): void { + $vars = [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'post' => ['name' => 'Jane Doe', 'nickname' => 'Janey'], + 'urlParams' => ['user' => 'jw', 'name' => 'Johnny Weissmüller'], + 'method' => 'GET' + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame(3, count($request)); + $this->assertSame('Janey', $request->{'nickname'}); + $this->assertSame('Johnny Weissmüller', $request->{'name'}); + } + + + + public function testImmutableArrayAccess(): void { + $this->expectException(\RuntimeException::class); + + $vars = [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'method' => 'GET' + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $request['nickname'] = 'Janey'; + } + + + public function testImmutableMagicAccess(): void { + $this->expectException(\RuntimeException::class); + + $vars = [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'method' => 'GET' + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $request->{'nickname'} = 'Janey'; + } + + + public function testGetTheMethodRight(): void { + $this->expectException(\LogicException::class); + + $vars = [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'method' => 'GET', + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $request->post; + } + + public function testTheMethodIsRight(): void { + $vars = [ + 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], + 'method' => 'GET', + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('GET', $request->method); + $result = $request->get; + $this->assertSame('John Q. Public', $result['name']); + $this->assertSame('Joey', $result['nickname']); + } + + public function testJsonPost(): void { + global $data; + $data = '{"name": "John Q. Public", "nickname": "Joey"}'; + $vars = [ + 'method' => 'POST', + 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'] + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('POST', $request->method); + $result = $request->post; + $this->assertSame('John Q. Public', $result['name']); + $this->assertSame('Joey', $result['nickname']); + $this->assertSame('Joey', $request->params['nickname']); + $this->assertSame('Joey', $request['nickname']); + } + + public function testScimJsonPost(): void { + global $data; + $data = '{"userName":"testusername", "displayName":"Example User"}'; + $vars = [ + 'method' => 'POST', + 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'] + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('POST', $request->method); + $result = $request->post; + $this->assertSame('testusername', $result['userName']); + $this->assertSame('Example User', $result['displayName']); + $this->assertSame('Example User', $request->params['displayName']); + $this->assertSame('Example User', $request['displayName']); + } + + public function testCustomJsonPost(): void { + global $data; + $data = '{"propertyA":"sometestvalue", "propertyB":"someothertestvalue"}'; + + // Note: the content type used here is fictional and intended to check if the regex for JSON content types works fine + $vars = [ + 'method' => 'POST', + 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'] + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('POST', $request->method); + $result = $request->post; + $this->assertSame('sometestvalue', $result['propertyA']); + $this->assertSame('someothertestvalue', $result['propertyB']); + } + + public static function dataNotJsonData(): array { + return [ + ['this is not valid json'], + ['"just a string"'], + ['{"just a string"}'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNotJsonData')] + public function testNotJsonPost(string $testData): void { + global $data; + $data = $testData; + $vars = [ + 'method' => 'POST', + 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'] + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertEquals('POST', $request->method); + $result = $request->post; + // ensure there's no error attempting to decode the content + } + + public function testNotScimJsonPost(): void { + global $data; + $data = 'this is not valid scim json'; + $vars = [ + 'method' => 'POST', + 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'] + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertEquals('POST', $request->method); + $result = $request->post; + // ensure there's no error attempting to decode the content + } + + public function testNotCustomJsonPost(): void { + global $data; + $data = 'this is not valid json'; + $vars = [ + 'method' => 'POST', + 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'] + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertEquals('POST', $request->method); + $result = $request->post; + // ensure there's no error attempting to decode the content + } + + public function testPatch(): void { + global $data; + $data = http_build_query(['name' => 'John Q. Public', 'nickname' => 'Joey'], '', '&'); + + $vars = [ + 'method' => 'PATCH', + 'server' => ['CONTENT_TYPE' => 'application/x-www-form-urlencoded'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PATCH', $request->method); + $result = $request->patch; + + $this->assertSame('John Q. Public', $result['name']); + $this->assertSame('Joey', $result['nickname']); + } + + public function testJsonPatchAndPut(): void { + global $data; + + // PUT content + $data = '{"name": "John Q. Public", "nickname": "Joey"}'; + $vars = [ + 'method' => 'PUT', + 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PUT', $request->method); + $result = $request->put; + + $this->assertSame('John Q. Public', $result['name']); + $this->assertSame('Joey', $result['nickname']); + + // PATCH content + $data = '{"name": "John Q. Public", "nickname": null}'; + $vars = [ + 'method' => 'PATCH', + 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PATCH', $request->method); + $result = $request->patch; + + $this->assertSame('John Q. Public', $result['name']); + $this->assertSame(null, $result['nickname']); + } + + public function testScimJsonPatchAndPut(): void { + global $data; + + // PUT content + $data = '{"userName": "sometestusername", "displayName": "Example User"}'; + $vars = [ + 'method' => 'PUT', + 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PUT', $request->method); + $result = $request->put; + + $this->assertSame('sometestusername', $result['userName']); + $this->assertSame('Example User', $result['displayName']); + + // PATCH content + $data = '{"userName": "sometestusername", "displayName": null}'; + $vars = [ + 'method' => 'PATCH', + 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PATCH', $request->method); + $result = $request->patch; + + $this->assertSame('sometestusername', $result['userName']); + $this->assertSame(null, $result['displayName']); + } + + public function testCustomJsonPatchAndPut(): void { + global $data; + + // PUT content + $data = '{"propertyA": "sometestvalue", "propertyB": "someothertestvalue"}'; + $vars = [ + 'method' => 'PUT', + 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PUT', $request->method); + $result = $request->put; + + $this->assertSame('sometestvalue', $result['propertyA']); + $this->assertSame('someothertestvalue', $result['propertyB']); + + // PATCH content + $data = '{"propertyA": "sometestvalue", "propertyB": null}'; + $vars = [ + 'method' => 'PATCH', + 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PATCH', $request->method); + $result = $request->patch; + + $this->assertSame('sometestvalue', $result['propertyA']); + $this->assertSame(null, $result['propertyB']); + } + + public function testPutStream(): void { + global $data; + $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); + + $vars = [ + 'put' => $data, + 'method' => 'PUT', + 'server' => [ + 'CONTENT_TYPE' => 'image/png', + 'CONTENT_LENGTH' => (string)strlen($data) + ], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('PUT', $request->method); + $resource = $request->put; + $contents = stream_get_contents($resource); + $this->assertSame($data, $contents); + + try { + $resource = $request->put; + } catch (\LogicException $e) { + return; + } + $this->fail('Expected LogicException.'); + } + + + public function testSetUrlParameters(): void { + $vars = [ + 'post' => [], + 'method' => 'POST', + 'urlParams' => ['id' => '2'], + ]; + + $request = new Request( + $vars, + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $newParams = ['id' => '3', 'test' => 'test2']; + $request->setUrlParameters($newParams); + $this->assertSame('test2', $request->getParam('test')); + $this->assertEquals('3', $request->getParam('id')); + $this->assertEquals('3', $request->getParams()['id']); + } + + public static function dataGetRemoteAddress(): array { + return [ + 'IPv4 without trusted remote' => [ + [ + 'REMOTE_ADDR' => '10.0.0.2', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + [], + [], + '10.0.0.2', + ], + 'IPv4 without trusted headers' => [ + [ + 'REMOTE_ADDR' => '10.0.0.2', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['10.0.0.2'], + [], + '10.0.0.2', + ], + 'IPv4 with single trusted remote' => [ + [ + 'REMOTE_ADDR' => '10.0.0.2', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['10.0.0.2'], + ['HTTP_X_FORWARDED'], + '10.4.0.4', + ], + 'IPv6 with single trusted remote' => [ + [ + 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['2001:db8:85a3:8d3:1319:8a2e:370:7348'], + ['HTTP_X_FORWARDED'], + '10.4.0.4', + ], + 'IPv4 with multiple trusted remotes' => [ + [ + 'REMOTE_ADDR' => '10.0.0.2', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4, ::1', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['10.0.0.2', '::1'], + ['HTTP_X_FORWARDED'], + '10.4.0.4', + ], + 'IPv4 order of forwarded-for headers' => [ + [ + 'REMOTE_ADDR' => '10.0.0.2', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['10.0.0.2'], + [ + 'HTTP_X_FORWARDED', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_CLIENT_IP', + ], + '192.168.0.233', + ], + 'IPv4 order of forwarded-for headers (reversed)' => [ + [ + 'REMOTE_ADDR' => '10.0.0.2', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['10.0.0.2'], + [ + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + ], + '10.4.0.4', + ], + 'IPv6 order of forwarded-for headers' => [ + [ + 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['2001:db8:85a3:8d3:1319:8a2e:370:7348'], + [ + 'HTTP_X_FORWARDED', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_CLIENT_IP', + ], + '192.168.0.233', + ], + 'IPv4 matching CIDR of trusted proxy' => [ + [ + 'REMOTE_ADDR' => '192.168.3.99', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['192.168.2.0/24'], + ['HTTP_X_FORWARDED_FOR'], + '192.168.3.99', + ], + 'IPv6 matching CIDR of trusted proxy' => [ + [ + 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a21:370:7348', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['2001:db8:85a3:8d3:1319:8a20::/95'], + ['HTTP_X_FORWARDED_FOR'], + '192.168.0.233', + ], + 'IPv6 not matching CIDR of trusted proxy' => [ + [ + 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['fd::/8'], + [], + '2001:db8:85a3:8d3:1319:8a2e:370:7348', + ], + 'IPv6 with invalid trusted proxy' => [ + [ + 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', + 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', + ], + ['fx::/8'], + [], + '2001:db8:85a3:8d3:1319:8a2e:370:7348', + ], + 'IPv4 forwarded for IPv6' => [ + [ + 'REMOTE_ADDR' => '192.168.2.99', + 'HTTP_X_FORWARDED_FOR' => '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + ], + ['192.168.2.0/24'], + ['HTTP_X_FORWARDED_FOR'], + '2001:db8:85a3:8d3:1319:8a2e:370:7348', + ], + 'IPv4 with port' => [ + [ + 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', + 'HTTP_X_FORWARDED_FOR' => '192.168.2.99:8080', + ], + ['2001:db8::/8'], + ['HTTP_X_FORWARDED_FOR'], + '192.168.2.99', + ], + 'IPv6 with port' => [ + [ + 'REMOTE_ADDR' => '192.168.2.99', + 'HTTP_X_FORWARDED_FOR' => '[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080', + ], + ['192.168.2.0/24'], + ['HTTP_X_FORWARDED_FOR'], + '2001:db8:85a3:8d3:1319:8a2e:370:7348', + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetRemoteAddress')] + public function testGetRemoteAddress(array $headers, array $trustedProxies, array $forwardedForHeaders, string $expected): void { + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['trusted_proxies', [], $trustedProxies], + ['forwarded_for_headers', ['HTTP_X_FORWARDED_FOR'], $forwardedForHeaders], + ]); + + $request = new Request( + [ + 'server' => $headers, + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getRemoteAddress()); + } + + public static function dataHttpProtocol(): array { + return [ + // Valid HTTP 1.0 + ['HTTP/1.0', 'HTTP/1.0'], + ['http/1.0', 'HTTP/1.0'], + ['HTTp/1.0', 'HTTP/1.0'], + + // Valid HTTP 1.1 + ['HTTP/1.1', 'HTTP/1.1'], + ['http/1.1', 'HTTP/1.1'], + ['HTTp/1.1', 'HTTP/1.1'], + + // Valid HTTP 2.0 + ['HTTP/2', 'HTTP/2'], + ['http/2', 'HTTP/2'], + ['HTTp/2', 'HTTP/2'], + + // Invalid + ['HTTp/394', 'HTTP/1.1'], + ['InvalidProvider/1.1', 'HTTP/1.1'], + [null, 'HTTP/1.1'], + ['', 'HTTP/1.1'], + + ]; + } + + /** + * + * @param mixed $input + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataHttpProtocol')] + public function testGetHttpProtocol($input, $expected): void { + $request = new Request( + [ + 'server' => [ + 'SERVER_PROTOCOL' => $input, + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getHttpProtocol()); + } + + public function testGetServerProtocolWithOverrideValid(): void { + $this->config + ->expects($this->exactly(3)) + ->method('getSystemValueString') + ->willReturnMap([ + ['overwriteprotocol', '', 'HTTPS'], // should be automatically lowercased + ['overwritecondaddr', '', ''], + ]); + + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('https', $request->getServerProtocol()); + } + + public function testGetServerProtocolWithOverrideInValid(): void { + $this->config + ->expects($this->exactly(3)) + ->method('getSystemValueString') + ->willReturnMap([ + ['overwriteprotocol', '', 'bogusProtocol'], // should trigger fallback to http + ['overwritecondaddr', '', ''], + ]); + + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('http', $request->getServerProtocol()); + } + + public function testGetServerProtocolWithProtoValid(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } + + return $default; + }); + + $requestHttps = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_PROTO' => 'HtTpS', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + $requestHttp = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_PROTO' => 'HTTp', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + + $this->assertSame('https', $requestHttps->getServerProtocol()); + $this->assertSame('http', $requestHttp->getServerProtocol()); + } + + public function testGetServerProtocolWithHttpsServerValueOn(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTPS' => 'on' + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + $this->assertSame('https', $request->getServerProtocol()); + } + + public function testGetServerProtocolWithHttpsServerValueOff(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTPS' => 'off' + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + $this->assertSame('http', $request->getServerProtocol()); + } + + public function testGetServerProtocolWithHttpsServerValueEmpty(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTPS' => '' + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + $this->assertSame('http', $request->getServerProtocol()); + } + + public function testGetServerProtocolDefault(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + return $default; + }); + + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + $this->assertSame('http', $request->getServerProtocol()); + } + + public function testGetServerProtocolBehindLoadBalancers(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } + + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_PROTO' => 'https,http,http', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('https', $request->getServerProtocol()); + } + + /** + * @param string $testAgent + * @param array $userAgent + * @param bool $matches + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataUserAgent')] + public function testUserAgent($testAgent, $userAgent, $matches): void { + $request = new Request( + [ + 'server' => [ + 'HTTP_USER_AGENT' => $testAgent, + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($matches, $request->isUserAgent($userAgent)); + } + + /** + * @param string $testAgent + * @param array $userAgent + * @param bool $matches + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataUserAgent')] + public function testUndefinedUserAgent($testAgent, $userAgent, $matches): void { + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertFalse($request->isUserAgent($userAgent)); + } + + public static function dataUserAgent(): array { + return [ + [ + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)', + [ + Request::USER_AGENT_IE + ], + true, + ], + [ + 'Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0', + [ + Request::USER_AGENT_IE + ], + false, + ], + [ + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', + [ + Request::USER_AGENT_CHROME + ], + true, + ], + [ + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/53.0.2785.143 Chrome/53.0.2785.143 Safari/537.36', + [ + Request::USER_AGENT_CHROME + ], + true, + ], + [ + 'Mozilla/5.0 (Linux; Android 4.4; Nexus 4 Build/KRT16S) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36', + [ + Request::USER_AGENT_ANDROID_MOBILE_CHROME + ], + true, + ], + [ + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)', + [ + Request::USER_AGENT_ANDROID_MOBILE_CHROME + ], + false, + ], + [ + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)', + [ + Request::USER_AGENT_IE, + Request::USER_AGENT_ANDROID_MOBILE_CHROME, + ], + true, + ], + [ + 'Mozilla/5.0 (Linux; Android 4.4; Nexus 4 Build/KRT16S) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36', + [ + Request::USER_AGENT_IE, + Request::USER_AGENT_ANDROID_MOBILE_CHROME, + ], + true, + ], + [ + 'Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0', + [ + Request::USER_AGENT_FREEBOX + ], + false, + ], + [ + 'Mozilla/5.0', + [ + Request::USER_AGENT_FREEBOX + ], + true, + ], + [ + 'Fake Mozilla/5.0', + [ + Request::USER_AGENT_FREEBOX + ], + false, + ], + [ + 'Mozilla/5.0 (Android) ownCloud-android/2.0.0', + [ + Request::USER_AGENT_CLIENT_ANDROID + ], + true, + ], + [ + 'Mozilla/5.0 (Android) Nextcloud-android/2.0.0', + [ + Request::USER_AGENT_CLIENT_ANDROID + ], + true, + ], + [ + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.99 Safari/537.36 Vivaldi/2.9.1705.41', + [ + Request::USER_AGENT_CHROME + ], + true + ], + [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.38 Safari/537.36 Brave/75', + [ + Request::USER_AGENT_CHROME + ], + true + ], + [ + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 OPR/50.0.2762.67', + [ + Request::USER_AGENT_CHROME + ], + true + ] + ]; + } + + public static function dataMatchClientVersion(): array { + return [ + [ + 'Mozilla/5.0 (Android) Nextcloud-android/3.24.1', + Request::USER_AGENT_CLIENT_ANDROID, + '3.24.1', + ], + [ + 'Mozilla/5.0 (iOS) Nextcloud-iOS/4.8.2', + Request::USER_AGENT_CLIENT_IOS, + '4.8.2', + ], + [ + 'Mozilla/5.0 (Windows) mirall/3.8.1', + Request::USER_AGENT_CLIENT_DESKTOP, + '3.8.1', + ], + [ + 'Mozilla/5.0 (Android) Nextcloud-Talk v17.10.0', + Request::USER_AGENT_TALK_ANDROID, + '17.10.0', + ], + [ + 'Mozilla/5.0 (iOS) Nextcloud-Talk v17.0.1', + Request::USER_AGENT_TALK_IOS, + '17.0.1', + ], + [ + 'Mozilla/5.0 (Windows) Nextcloud-Talk v0.6.0', + Request::USER_AGENT_TALK_DESKTOP, + '0.6.0', + ], + [ + 'Mozilla/5.0 (Windows) Nextcloud-Outlook v1.0.0', + Request::USER_AGENT_OUTLOOK_ADDON, + '1.0.0', + ], + [ + 'Filelink for *cloud/1.0.0', + Request::USER_AGENT_THUNDERBIRD_ADDON, + '1.0.0', + ], + ]; + } + + /** + * @param string $testAgent + * @param string $userAgent + * @param string $version + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataMatchClientVersion')] + public function testMatchClientVersion(string $testAgent, string $userAgent, string $version): void { + preg_match($userAgent, $testAgent, $matches); + + $this->assertSame($version, $matches[1]); + } + + public function testInsecureServerHostServerNameHeader(): void { + $request = new Request( + [ + 'server' => [ + 'SERVER_NAME' => 'from.server.name:8080', + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('from.server.name:8080', $request->getInsecureServerHost()); + } + + public function testInsecureServerHostHttpHostHeader(): void { + $request = new Request( + [ + 'server' => [ + 'SERVER_NAME' => 'from.server.name:8080', + 'HTTP_HOST' => 'from.host.header:8080', + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('from.host.header:8080', $request->getInsecureServerHost()); + } + + public function testInsecureServerHostHttpFromForwardedHeaderSingle(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } + + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'SERVER_NAME' => 'from.server.name:8080', + 'HTTP_HOST' => 'from.host.header:8080', + 'HTTP_X_FORWARDED_HOST' => 'from.forwarded.host:8080', + 'REMOTE_ADDR' => '1.2.3.4', + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('from.forwarded.host:8080', $request->getInsecureServerHost()); + } + + public function testInsecureServerHostHttpFromForwardedHeaderStacked(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } + + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'SERVER_NAME' => 'from.server.name:8080', + 'HTTP_HOST' => 'from.host.header:8080', + 'HTTP_X_FORWARDED_HOST' => 'from.forwarded.host2:8080,another.one:9000', + 'REMOTE_ADDR' => '1.2.3.4', + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('from.forwarded.host2:8080', $request->getInsecureServerHost()); + } + + public function testGetServerHostWithOverwriteHost(): void { + $this->config + ->method('getSystemValueString') + ->willReturnCallback(function ($key, $default) { + if ($key === 'overwritecondaddr') { + return ''; + } elseif ($key === 'overwritehost') { + return 'my.overwritten.host'; + } + + return $default; + }); + + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('my.overwritten.host', $request->getServerHost()); + } + + public function testGetServerHostWithTrustedDomain(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } elseif ($key === 'trusted_domains') { + return ['my.trusted.host']; + } + + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_HOST' => 'my.trusted.host', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('my.trusted.host', $request->getServerHost()); + } + + public function testGetServerHostWithUntrustedDomain(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } elseif ($key === 'trusted_domains') { + return ['my.trusted.host']; + } + + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_HOST' => 'my.untrusted.host', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('my.trusted.host', $request->getServerHost()); + } + + public function testGetServerHostWithNoTrustedDomain(): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_HOST' => 'my.untrusted.host', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('', $request->getServerHost()); + } + + public static function dataGetServerHostTrustedDomain(): array { + return [ + 'is array' => ['my.trusted.host', ['my.trusted.host']], + 'is array but undefined index 0' => ['my.trusted.host', [2 => 'my.trusted.host']], + 'is string' => ['my.trusted.host', 'my.trusted.host'], + 'is null' => ['', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetServerHostTrustedDomain')] + public function testGetServerHostTrustedDomain(string $expected, $trustedDomain): void { + $this->config + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) use ($trustedDomain) { + if ($key === 'trusted_proxies') { + return ['1.2.3.4']; + } + if ($key === 'trusted_domains') { + return $trustedDomain; + } + return $default; + }); + + $request = new Request( + [ + 'server' => [ + 'HTTP_X_FORWARDED_HOST' => 'my.untrusted.host', + 'REMOTE_ADDR' => '1.2.3.4', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getServerHost()); + } + + public function testGetOverwriteHostDefaultNull(): void { + $this->config + ->expects($this->once()) + ->method('getSystemValueString') + ->with('overwritehost') + ->willReturn(''); + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertNull(self::invokePrivate($request, 'getOverwriteHost')); + } + + public function testGetOverwriteHostWithOverwrite(): void { + $this->config + ->expects($this->exactly(3)) + ->method('getSystemValueString') + ->willReturnMap([ + ['overwritehost', '', 'www.owncloud.org'], + ['overwritecondaddr', '', ''], + ]); + + $request = new Request( + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('www.owncloud.org', self::invokePrivate($request, 'getOverwriteHost')); + } + + + public function testGetPathInfoNotProcessible(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The requested uri(/foo.php) cannot be processed by the script \'/var/www/index.php\')'); + + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => '/foo.php', + 'SCRIPT_NAME' => '/var/www/index.php', + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $request->getPathInfo(); + } + + + public function testGetRawPathInfoNotProcessible(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The requested uri(/foo.php) cannot be processed by the script \'/var/www/index.php\')'); + + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => '/foo.php', + 'SCRIPT_NAME' => '/var/www/index.php', + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $request->getRawPathInfo(); + } + + /** + * @param string $requestUri + * @param string $scriptName + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenericPathInfo')] + public function testGetPathInfoWithoutSetEnvGeneric($requestUri, $scriptName, $expected): void { + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => $requestUri, + 'SCRIPT_NAME' => $scriptName, + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getPathInfo()); + } + + /** + * @param string $requestUri + * @param string $scriptName + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenericPathInfo')] + public function testGetRawPathInfoWithoutSetEnvGeneric($requestUri, $scriptName, $expected): void { + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => $requestUri, + 'SCRIPT_NAME' => $scriptName, + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getRawPathInfo()); + } + + /** + * @param string $requestUri + * @param string $scriptName + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataRawPathInfo')] + public function testGetRawPathInfoWithoutSetEnv($requestUri, $scriptName, $expected): void { + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => $requestUri, + 'SCRIPT_NAME' => $scriptName, + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getRawPathInfo()); + } + + /** + * @param string $requestUri + * @param string $scriptName + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPathInfo')] + public function testGetPathInfoWithoutSetEnv($requestUri, $scriptName, $expected): void { + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => $requestUri, + 'SCRIPT_NAME' => $scriptName, + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame($expected, $request->getPathInfo()); + } + + public static function dataGenericPathInfo(): array { + return [ + ['/core/index.php?XDEBUG_SESSION_START=14600', '/core/index.php', ''], + ['/index.php/apps/files/', 'index.php', '/apps/files/'], + ['/index.php/apps/files/../&/&?someQueryParameter=QueryParam', 'index.php', '/apps/files/../&/&'], + ['/remote.php/漢字編碼方法 / 汉字编码方法', 'remote.php', '/漢字編碼方法 / 汉字编码方法'], + ['///removeTrailin//gSlashes///', 'remote.php', '/removeTrailin/gSlashes/'], + ['/', '/', ''], + ['', '', ''], + ]; + } + + public static function dataRawPathInfo(): array { + return [ + ['/foo%2Fbar/subfolder', '', 'foo%2Fbar/subfolder'], + ]; + } + + public static function dataPathInfo(): array { + return [ + ['/foo%2Fbar/subfolder', '', 'foo/bar/subfolder'], + ]; + } + + public function testGetRequestUriWithoutOverwrite(): void { + $this->config + ->expects($this->once()) + ->method('getSystemValueString') + ->with('overwritewebroot') + ->willReturn(''); + + $request = new Request( + [ + 'server' => [ + 'REQUEST_URI' => '/test.php' + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ); + + $this->assertSame('/test.php', $request->getRequestUri()); + } + + public static function dataGetRequestUriWithOverwrite(): array { + return [ + ['/scriptname.php/some/PathInfo', '/owncloud/', ''], + ['/scriptname.php/some/PathInfo', '/owncloud/', '123', '123.123.123.123'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetRequestUriWithOverwrite')] + public function testGetRequestUriWithOverwrite($expectedUri, $overwriteWebRoot, $overwriteCondAddr, $remoteAddr = ''): void { + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnMap([ + ['overwritewebroot', '', $overwriteWebRoot], + ['overwritecondaddr', '', $overwriteCondAddr], + ]); + + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'REQUEST_URI' => '/test.php/some/PathInfo', + 'SCRIPT_NAME' => '/test.php', + 'REMOTE_ADDR' => $remoteAddr + ] + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $request + ->expects($this->once()) + ->method('getScriptName') + ->willReturn('/scriptname.php'); + + $this->assertSame($expectedUri, $request->getRequestUri()); + } + + public function testPassesCSRFCheckWithGet(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'get' => [ + 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + 'nc_sameSiteCookiestrict' => 'true', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->with($token) + ->willReturn(true); + + $this->assertTrue($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithPost(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'post' => [ + 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + 'nc_sameSiteCookiestrict' => 'true', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->with($token) + ->willReturn(true); + + $this->assertTrue($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithHeader(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + 'nc_sameSiteCookiestrict' => 'true', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->with($token) + ->willReturn(true); + + $this->assertTrue($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithGetAndWithoutCookies(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'get' => [ + 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->willReturn(true); + + $this->assertTrue($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithPostAndWithoutCookies(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'post' => [ + 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->willReturn(true); + + $this->assertTrue($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithHeaderAndWithoutCookies(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->willReturn(true); + + $this->assertTrue($request->passesCSRFCheck()); + } + + public function testFailsCSRFCheckWithHeaderAndNotAllChecksPassing(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookiestrict' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $this->csrfTokenManager + ->expects($this->never()) + ->method('isTokenValid'); + + $this->assertFalse($request->passesCSRFCheck()); + } + + public function testPassesStrictCookieCheckWithAllCookiesAndStrict(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName', 'getCookieParams']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + '__Host-nc_sameSiteCookiestrict' => 'true', + '__Host-nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $request + ->expects($this->any()) + ->method('getCookieParams') + ->willReturn([ + 'secure' => true, + 'path' => '/', + ]); + + $this->assertTrue($request->passesStrictCookieCheck()); + } + + public function testFailsStrictCookieCheckWithAllCookiesAndMissingStrict(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName', 'getCookieParams']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookiestrict' => 'true', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $request + ->expects($this->any()) + ->method('getCookieParams') + ->willReturn([ + 'secure' => true, + 'path' => '/', + ]); + + $this->assertFalse($request->passesStrictCookieCheck()); + } + + public function testGetCookieParams(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $actual = $request->getCookieParams(); + $this->assertSame(session_get_cookie_params(), $actual); + } + + public function testPassesStrictCookieCheckWithAllCookies(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookiestrict' => 'true', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertTrue($request->passesStrictCookieCheck()); + } + + public function testPassesStrictCookieCheckWithRandomCookies(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + 'RandomCookie' => 'asdf', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertTrue($request->passesStrictCookieCheck()); + } + + public function testFailsStrictCookieCheckWithSessionCookie(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertFalse($request->passesStrictCookieCheck()); + } + + public function testFailsStrictCookieCheckWithRememberMeCookie(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + 'nc_token' => 'asdf', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertFalse($request->passesStrictCookieCheck()); + } + + public function testFailsCSRFCheckWithPostAndWithCookies(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'post' => [ + 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'foo' => 'bar', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + $this->csrfTokenManager + ->expects($this->never()) + ->method('isTokenValid'); + + $this->assertFalse($request->passesCSRFCheck()); + } + + public function testFailStrictCookieCheckWithOnlyLaxCookie(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertFalse($request->passesStrictCookieCheck()); + } + + public function testFailStrictCookieCheckWithOnlyStrictCookie(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookiestrict' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertFalse($request->passesStrictCookieCheck()); + } + + public function testPassesLaxCookieCheck(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookielax' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertTrue($request->passesLaxCookieCheck()); + } + + public function testFailsLaxCookieCheckWithOnlyStrictCookie(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookiestrict' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertFalse($request->passesLaxCookieCheck()); + } + + public function testSkipCookieCheckForOCSRequests(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', + 'HTTP_OCS_APIREQUEST' => 'true', + ], + 'cookies' => [ + session_name() => 'asdf', + 'nc_sameSiteCookiestrict' => 'false', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertTrue($request->passesStrictCookieCheck()); + } + + public static function dataInvalidToken(): array { + return [ + ['InvalidSentToken'], + ['InvalidSentToken:InvalidSecret'], + [''], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataInvalidToken')] + public function testPassesCSRFCheckWithInvalidToken(string $invalidToken): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_REQUESTTOKEN' => $invalidToken, + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $token = new CsrfToken($invalidToken); + $this->csrfTokenManager + ->expects($this->any()) + ->method('isTokenValid') + ->with($token) + ->willReturn(false); + + $this->assertFalse($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithoutTokenFail(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertFalse($request->passesCSRFCheck()); + } + + public function testPassesCSRFCheckWithOCSAPIRequestHeader(): void { + /** @var Request $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getScriptName']) + ->setConstructorArgs([ + [ + 'server' => [ + 'HTTP_OCS_APIREQUEST' => 'true', + ], + ], + $this->requestId, + $this->config, + $this->csrfTokenManager, + $this->stream + ]) + ->getMock(); + + $this->assertTrue($request->passesCSRFCheck()); + } +} diff --git a/tests/lib/AppFramework/Http/ResponseTest.php b/tests/lib/AppFramework/Http/ResponseTest.php new file mode 100644 index 00000000000..4c76695f6e4 --- /dev/null +++ b/tests/lib/AppFramework/Http/ResponseTest.php @@ -0,0 +1,277 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Utility\ITimeFactory; + +class ResponseTest extends \Test\TestCase { + /** + * @var Response + */ + private $childResponse; + + protected function setUp(): void { + parent::setUp(); + $this->childResponse = new Response(); + } + + + public function testAddHeader(): void { + $this->childResponse->addHeader(' hello ', 'world'); + $headers = $this->childResponse->getHeaders(); + $this->assertEquals('world', $headers['hello']); + } + + + public function testSetHeaders(): void { + $expected = [ + 'Last-Modified' => 1, + 'ETag' => 3, + 'Something-Else' => 'hi', + 'X-Robots-Tag' => 'noindex, nofollow', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + ]; + + $this->childResponse->setHeaders($expected); + $expected['Content-Security-Policy'] = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; + $expected['Feature-Policy'] = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; + + $headers = $this->childResponse->getHeaders(); + unset($headers['X-Request-Id']); + + $this->assertEquals($expected, $headers); + } + + public function testOverwriteCsp(): void { + $expected = [ + 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-inline';style-src 'self' 'unsafe-inline';img-src 'self';font-src 'self' data:;connect-src 'self';media-src 'self'", + ]; + $policy = new ContentSecurityPolicy(); + + $this->childResponse->setContentSecurityPolicy($policy); + $headers = $this->childResponse->getHeaders(); + + $this->assertEquals(array_merge($expected, $headers), $headers); + } + + public function testGetCsp(): void { + $policy = new ContentSecurityPolicy(); + + $this->childResponse->setContentSecurityPolicy($policy); + $this->assertEquals($policy, $this->childResponse->getContentSecurityPolicy()); + } + + public function testGetCspEmpty(): void { + $this->assertEquals(new EmptyContentSecurityPolicy(), $this->childResponse->getContentSecurityPolicy()); + } + + public function testAddHeaderValueNullDeletesIt(): void { + $this->childResponse->addHeader('hello', 'world'); + $this->childResponse->addHeader('hello', null); + $this->assertEquals(5, count($this->childResponse->getHeaders())); + } + + + public function testCacheHeadersAreDisabledByDefault(): void { + $headers = $this->childResponse->getHeaders(); + $this->assertEquals('no-cache, no-store, must-revalidate', $headers['Cache-Control']); + } + + + public function testAddCookie(): void { + $this->childResponse->addCookie('foo', 'bar'); + $this->childResponse->addCookie('bar', 'foo', new \DateTime('1970-01-01')); + + $expectedResponse = [ + 'foo' => [ + 'value' => 'bar', + 'expireDate' => null, + 'sameSite' => 'Lax', + ], + 'bar' => [ + 'value' => 'foo', + 'expireDate' => new \DateTime('1970-01-01'), + 'sameSite' => 'Lax', + ] + ]; + $this->assertEquals($expectedResponse, $this->childResponse->getCookies()); + } + + + public function testSetCookies(): void { + $expected = [ + 'foo' => [ + 'value' => 'bar', + 'expireDate' => null, + ], + 'bar' => [ + 'value' => 'foo', + 'expireDate' => new \DateTime('1970-01-01') + ] + ]; + + $this->childResponse->setCookies($expected); + $cookies = $this->childResponse->getCookies(); + + $this->assertEquals($expected, $cookies); + } + + + public function testInvalidateCookie(): void { + $this->childResponse->addCookie('foo', 'bar'); + $this->childResponse->invalidateCookie('foo'); + $expected = [ + 'foo' => [ + 'value' => 'expired', + 'expireDate' => new \DateTime('1971-01-01'), + 'sameSite' => 'Lax', + ] + ]; + + $cookies = $this->childResponse->getCookies(); + + $this->assertEquals($expected, $cookies); + } + + + public function testInvalidateCookies(): void { + $this->childResponse->addCookie('foo', 'bar'); + $this->childResponse->addCookie('bar', 'foo'); + $expected = [ + 'foo' => [ + 'value' => 'bar', + 'expireDate' => null, + 'sameSite' => 'Lax', + ], + 'bar' => [ + 'value' => 'foo', + 'expireDate' => null, + 'sameSite' => 'Lax', + ] + ]; + $cookies = $this->childResponse->getCookies(); + $this->assertEquals($expected, $cookies); + + $this->childResponse->invalidateCookies(['foo', 'bar']); + $expected = [ + 'foo' => [ + 'value' => 'expired', + 'expireDate' => new \DateTime('1971-01-01'), + 'sameSite' => 'Lax', + ], + 'bar' => [ + 'value' => 'expired', + 'expireDate' => new \DateTime('1971-01-01'), + 'sameSite' => 'Lax', + ] + ]; + + $cookies = $this->childResponse->getCookies(); + $this->assertEquals($expected, $cookies); + } + + + public function testRenderReturnNullByDefault(): void { + $this->assertEquals(null, $this->childResponse->render()); + } + + + public function testGetStatus(): void { + $default = $this->childResponse->getStatus(); + + $this->childResponse->setStatus(Http::STATUS_NOT_FOUND); + + $this->assertEquals(Http::STATUS_OK, $default); + $this->assertEquals(Http::STATUS_NOT_FOUND, $this->childResponse->getStatus()); + } + + + public function testGetEtag(): void { + $this->childResponse->setEtag('hi'); + $this->assertSame('hi', $this->childResponse->getEtag()); + } + + + public function testGetLastModified(): void { + $lastModified = new \DateTime('now', new \DateTimeZone('GMT')); + $lastModified->setTimestamp(1); + $this->childResponse->setLastModified($lastModified); + $this->assertEquals($lastModified, $this->childResponse->getLastModified()); + } + + + + public function testCacheSecondsZero(): void { + $this->childResponse->cacheFor(0); + + $headers = $this->childResponse->getHeaders(); + $this->assertEquals('no-cache, no-store, must-revalidate', $headers['Cache-Control']); + $this->assertFalse(isset($headers['Expires'])); + } + + + public function testCacheSeconds(): void { + $time = $this->createMock(ITimeFactory::class); + $time->method('getTime') + ->willReturn(1234567); + + $this->overwriteService(ITimeFactory::class, $time); + + $this->childResponse->cacheFor(33); + + $headers = $this->childResponse->getHeaders(); + $this->assertEquals('private, max-age=33, must-revalidate', $headers['Cache-Control']); + $this->assertEquals('Thu, 15 Jan 1970 06:56:40 GMT', $headers['Expires']); + } + + + + public function testEtagLastModifiedHeaders(): void { + $lastModified = new \DateTime('now', new \DateTimeZone('GMT')); + $lastModified->setTimestamp(1); + $this->childResponse->setLastModified($lastModified); + $headers = $this->childResponse->getHeaders(); + $this->assertEquals('Thu, 01 Jan 1970 00:00:01 GMT', $headers['Last-Modified']); + } + + public function testChainability(): void { + $lastModified = new \DateTime('now', new \DateTimeZone('GMT')); + $lastModified->setTimestamp(1); + + $this->childResponse->setEtag('hi') + ->setStatus(Http::STATUS_NOT_FOUND) + ->setLastModified($lastModified) + ->cacheFor(33) + ->addHeader('hello', 'world'); + + $headers = $this->childResponse->getHeaders(); + + $this->assertEquals('world', $headers['hello']); + $this->assertEquals(Http::STATUS_NOT_FOUND, $this->childResponse->getStatus()); + $this->assertEquals('hi', $this->childResponse->getEtag()); + $this->assertEquals('Thu, 01 Jan 1970 00:00:01 GMT', $headers['Last-Modified']); + $this->assertEquals('private, max-age=33, must-revalidate', + $headers['Cache-Control']); + } + + public function testThrottle(): void { + $this->assertFalse($this->childResponse->isThrottled()); + $this->childResponse->throttle(); + $this->assertTrue($this->childResponse->isThrottled()); + } + + public function testGetThrottleMetadata(): void { + $this->childResponse->throttle(['foo' => 'bar']); + $this->assertSame(['foo' => 'bar'], $this->childResponse->getThrottleMetadata()); + } +} diff --git a/tests/lib/AppFramework/Http/StreamResponseTest.php b/tests/lib/AppFramework/Http/StreamResponseTest.php new file mode 100644 index 00000000000..87f6097a07a --- /dev/null +++ b/tests/lib/AppFramework/Http/StreamResponseTest.php @@ -0,0 +1,82 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\IOutput; +use OCP\AppFramework\Http\StreamResponse; + +class StreamResponseTest extends \Test\TestCase { + /** @var IOutput */ + private $output; + + protected function setUp(): void { + parent::setUp(); + $this->output = $this->getMockBuilder('OCP\\AppFramework\\Http\\IOutput') + ->disableOriginalConstructor() + ->getMock(); + } + + public function testOutputNotModified(): void { + $path = __FILE__; + $this->output->expects($this->once()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_NOT_MODIFIED); + $this->output->expects($this->never()) + ->method('setReadfile'); + $response = new StreamResponse($path); + + $response->callback($this->output); + } + + public function testOutputOk(): void { + $path = __FILE__; + $this->output->expects($this->once()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_OK); + $this->output->expects($this->once()) + ->method('setReadfile') + ->with($this->equalTo($path)) + ->willReturn(true); + $response = new StreamResponse($path); + + $response->callback($this->output); + } + + public function testOutputNotFound(): void { + $path = __FILE__ . 'test'; + $this->output->expects($this->once()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_OK); + $this->output->expects($this->never()) + ->method('setReadfile'); + $this->output->expects($this->once()) + ->method('setHttpResponseCode') + ->with($this->equalTo(Http::STATUS_NOT_FOUND)); + $response = new StreamResponse($path); + + $response->callback($this->output); + } + + public function testOutputReadFileError(): void { + $path = __FILE__; + $this->output->expects($this->once()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_OK); + $this->output->expects($this->once()) + ->method('setReadfile') + ->willReturn(false); + $this->output->expects($this->once()) + ->method('setHttpResponseCode') + ->with($this->equalTo(Http::STATUS_BAD_REQUEST)); + $response = new StreamResponse($path); + + $response->callback($this->output); + } +} diff --git a/tests/lib/AppFramework/Http/TemplateResponseTest.php b/tests/lib/AppFramework/Http/TemplateResponseTest.php new file mode 100644 index 00000000000..28f952e35e3 --- /dev/null +++ b/tests/lib/AppFramework/Http/TemplateResponseTest.php @@ -0,0 +1,69 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\TemplateResponse; + +class TemplateResponseTest extends \Test\TestCase { + /** + * @var TemplateResponse + */ + private $tpl; + + protected function setUp(): void { + parent::setUp(); + + $this->tpl = new TemplateResponse('app', 'home'); + } + + + public function testSetParamsConstructor(): void { + $params = ['hi' => 'yo']; + $this->tpl = new TemplateResponse('app', 'home', $params); + + $this->assertEquals(['hi' => 'yo'], $this->tpl->getParams()); + } + + + public function testSetRenderAsConstructor(): void { + $renderAs = 'myrender'; + $this->tpl = new TemplateResponse('app', 'home', [], $renderAs); + + $this->assertEquals($renderAs, $this->tpl->getRenderAs()); + } + + + public function testSetParams(): void { + $params = ['hi' => 'yo']; + $this->tpl->setParams($params); + + $this->assertEquals(['hi' => 'yo'], $this->tpl->getParams()); + } + + + public function testGetTemplateName(): void { + $this->assertEquals('home', $this->tpl->getTemplateName()); + } + + public function testGetRenderAs(): void { + $render = 'myrender'; + $this->tpl->renderAs($render); + $this->assertEquals($render, $this->tpl->getRenderAs()); + } + + public function testChainability(): void { + $params = ['hi' => 'yo']; + $this->tpl->setParams($params) + ->setStatus(Http::STATUS_NOT_FOUND); + + $this->assertEquals(Http::STATUS_NOT_FOUND, $this->tpl->getStatus()); + $this->assertEquals(['hi' => 'yo'], $this->tpl->getParams()); + } +} diff --git a/tests/lib/AppFramework/Middleware/AdditionalScriptsMiddlewareTest.php b/tests/lib/AppFramework/Middleware/AdditionalScriptsMiddlewareTest.php new file mode 100644 index 00000000000..4fa5de62b0b --- /dev/null +++ b/tests/lib/AppFramework/Middleware/AdditionalScriptsMiddlewareTest.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\Middleware\AdditionalScriptsMiddleware; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\StandaloneTemplateResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\PublicShareController; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; + +class AdditionalScriptsMiddlewareTest extends \Test\TestCase { + /** @var IUserSession|MockObject */ + private $userSession; + + /** @var Controller */ + private $controller; + + /** @var AdditionalScriptsMiddleware */ + private $middleWare; + /** @var IEventDispatcher|MockObject */ + private $dispatcher; + + protected function setUp(): void { + parent::setUp(); + + $this->userSession = $this->createMock(IUserSession::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + $this->middleWare = new AdditionalScriptsMiddleware( + $this->userSession, + $this->dispatcher + ); + + $this->controller = $this->createMock(Controller::class); + } + + public function testNoTemplateResponse(): void { + $this->userSession->expects($this->never()) + ->method($this->anything()); + $this->dispatcher->expects($this->never()) + ->method($this->anything()); + + $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(Response::class)); + } + + public function testPublicShareController(): void { + $this->userSession->expects($this->never()) + ->method($this->anything()); + $this->dispatcher->expects($this->never()) + ->method($this->anything()); + + $this->middleWare->afterController($this->createMock(PublicShareController::class), 'myMethod', $this->createMock(Response::class)); + } + + public function testStandaloneTemplateResponse(): void { + $this->userSession->expects($this->never()) + ->method($this->anything()); + $this->dispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function ($event): void { + if ($event instanceof BeforeTemplateRenderedEvent && $event->isLoggedIn() === false) { + return; + } + + $this->fail('Wrong event dispatched'); + }); + + $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(StandaloneTemplateResponse::class)); + } + + public function testTemplateResponseNotLoggedIn(): void { + $this->userSession->method('isLoggedIn') + ->willReturn(false); + $this->dispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function ($event): void { + if ($event instanceof BeforeTemplateRenderedEvent && $event->isLoggedIn() === false) { + return; + } + + $this->fail('Wrong event dispatched'); + }); + + $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(TemplateResponse::class)); + } + + public function testTemplateResponseLoggedIn(): void { + $events = []; + + $this->userSession->method('isLoggedIn') + ->willReturn(true); + $this->dispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function ($event): void { + if ($event instanceof BeforeTemplateRenderedEvent && $event->isLoggedIn() === true) { + return; + } + + $this->fail('Wrong event dispatched'); + }); + + $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(TemplateResponse::class)); + } +} diff --git a/tests/lib/AppFramework/Middleware/CompressionMiddlewareTest.php b/tests/lib/AppFramework/Middleware/CompressionMiddlewareTest.php new file mode 100644 index 00000000000..010ce3fff6d --- /dev/null +++ b/tests/lib/AppFramework/Middleware/CompressionMiddlewareTest.php @@ -0,0 +1,145 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\Middleware\CompressionMiddleware; +use OC\AppFramework\OCS\V1Response; +use OC\AppFramework\OCS\V2Response; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +class CompressionMiddlewareTest extends \Test\TestCase { + /** @var IRequest */ + private $request; + /** @var Controller */ + private $controller; + /** @var CompressionMiddleware */ + private $middleWare; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->middleWare = new CompressionMiddleware( + $this->request + ); + + $this->controller = $this->createMock(Controller::class); + } + + public function testGzipOCSV1(): void { + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip'); + + $response = $this->createMock(V1Response::class); + $response->expects($this->once()) + ->method('addHeader') + ->with('Content-Encoding', 'gzip'); + + $response->method('getStatus') + ->willReturn(Http::STATUS_OK); + + $this->middleWare->beforeController($this->controller, 'myMethod'); + $this->middleWare->afterController($this->controller, 'myMethod', $response); + + $output = 'myoutput'; + $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); + + $this->assertSame($output, gzdecode($result)); + } + + public function testGzipOCSV2(): void { + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip'); + + $response = $this->createMock(V2Response::class); + $response->expects($this->once()) + ->method('addHeader') + ->with('Content-Encoding', 'gzip'); + + $response->method('getStatus') + ->willReturn(Http::STATUS_OK); + + $this->middleWare->beforeController($this->controller, 'myMethod'); + $this->middleWare->afterController($this->controller, 'myMethod', $response); + + $output = 'myoutput'; + $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); + + $this->assertSame($output, gzdecode($result)); + } + + public function testGzipJSONResponse(): void { + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip'); + + $response = $this->createMock(JSONResponse::class); + $response->expects($this->once()) + ->method('addHeader') + ->with('Content-Encoding', 'gzip'); + + $response->method('getStatus') + ->willReturn(Http::STATUS_OK); + + $this->middleWare->beforeController($this->controller, 'myMethod'); + $this->middleWare->afterController($this->controller, 'myMethod', $response); + + $output = 'myoutput'; + $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); + + $this->assertSame($output, gzdecode($result)); + } + + public function testNoGzipDataResponse(): void { + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip'); + + $response = $this->createMock(DataResponse::class); + $response->expects($this->never()) + ->method('addHeader'); + + $response->method('getStatus') + ->willReturn(Http::STATUS_OK); + $this->middleWare->beforeController($this->controller, 'myMethod'); + $this->middleWare->afterController($this->controller, 'myMethod', $response); + + $output = 'myoutput'; + $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); + + $this->assertSame($output, $result); + } + + public function testNoGzipNo200(): void { + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip'); + + $response = $this->createMock(JSONResponse::class); + $response->expects($this->never()) + ->method('addHeader'); + + $response->method('getStatus') + ->willReturn(Http::STATUS_NOT_FOUND); + + $this->middleWare->beforeController($this->controller, 'myMethod'); + $this->middleWare->afterController($this->controller, 'myMethod', $response); + + $output = 'myoutput'; + $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); + + $this->assertSame($output, $result); + } +} diff --git a/tests/lib/AppFramework/Middleware/MiddlewareDispatcherTest.php b/tests/lib/AppFramework/Middleware/MiddlewareDispatcherTest.php new file mode 100644 index 00000000000..aae1c53456b --- /dev/null +++ b/tests/lib/AppFramework/Middleware/MiddlewareDispatcherTest.php @@ -0,0 +1,282 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\Http\Request; +use OC\AppFramework\Middleware\MiddlewareDispatcher; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\IConfig; +use OCP\IRequestId; + +// needed to test ordering +class TestMiddleware extends Middleware { + public static $beforeControllerCalled = 0; + public static $afterControllerCalled = 0; + public static $afterExceptionCalled = 0; + public static $beforeOutputCalled = 0; + + public $beforeControllerOrder = 0; + public $afterControllerOrder = 0; + public $afterExceptionOrder = 0; + public $beforeOutputOrder = 0; + + public $controller; + public $methodName; + public $exception; + public $response; + public $output; + + /** + * @param boolean $beforeControllerThrowsEx + */ + public function __construct( + private $beforeControllerThrowsEx, + ) { + self::$beforeControllerCalled = 0; + self::$afterControllerCalled = 0; + self::$afterExceptionCalled = 0; + self::$beforeOutputCalled = 0; + } + + public function beforeController($controller, $methodName) { + self::$beforeControllerCalled++; + $this->beforeControllerOrder = self::$beforeControllerCalled; + $this->controller = $controller; + $this->methodName = $methodName; + if ($this->beforeControllerThrowsEx) { + throw new \Exception(); + } + } + + public function afterException($controller, $methodName, \Exception $exception) { + self::$afterExceptionCalled++; + $this->afterExceptionOrder = self::$afterExceptionCalled; + $this->controller = $controller; + $this->methodName = $methodName; + $this->exception = $exception; + parent::afterException($controller, $methodName, $exception); + } + + public function afterController($controller, $methodName, Response $response) { + self::$afterControllerCalled++; + $this->afterControllerOrder = self::$afterControllerCalled; + $this->controller = $controller; + $this->methodName = $methodName; + $this->response = $response; + return parent::afterController($controller, $methodName, $response); + } + + public function beforeOutput($controller, $methodName, $output) { + self::$beforeOutputCalled++; + $this->beforeOutputOrder = self::$beforeOutputCalled; + $this->controller = $controller; + $this->methodName = $methodName; + $this->output = $output; + return parent::beforeOutput($controller, $methodName, $output); + } +} + +class TestController extends Controller { + public function method(): void { + } +} + +class MiddlewareDispatcherTest extends \Test\TestCase { + public $exception; + public $response; + private $out; + private $method; + private $controller; + + /** + * @var MiddlewareDispatcher + */ + private $dispatcher; + + protected function setUp(): void { + parent::setUp(); + + $this->dispatcher = new MiddlewareDispatcher(); + $this->controller = $this->getControllerMock(); + $this->method = 'method'; + $this->response = new Response(); + $this->out = 'hi'; + $this->exception = new \Exception(); + } + + + private function getControllerMock() { + return $this->getMockBuilder(TestController::class) + ->onlyMethods(['method']) + ->setConstructorArgs(['app', + new Request( + ['method' => 'GET'], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ) + ])->getMock(); + } + + + private function getMiddleware($beforeControllerThrowsEx = false) { + $m1 = new TestMiddleware($beforeControllerThrowsEx); + $this->dispatcher->registerMiddleware($m1); + return $m1; + } + + + public function testAfterExceptionShouldReturnResponseOfMiddleware(): void { + $response = new Response(); + $m1 = $this->getMockBuilder(Middleware::class) + ->onlyMethods(['afterException', 'beforeController']) + ->getMock(); + $m1->expects($this->never()) + ->method('afterException'); + + $m2 = $this->getMockBuilder(Middleware::class) + ->onlyMethods(['afterException', 'beforeController']) + ->getMock(); + $m2->expects($this->once()) + ->method('afterException') + ->willReturn($response); + + $this->dispatcher->registerMiddleware($m1); + $this->dispatcher->registerMiddleware($m2); + + $this->dispatcher->beforeController($this->controller, $this->method); + $this->assertEquals($response, $this->dispatcher->afterException($this->controller, $this->method, $this->exception)); + } + + + public function testAfterExceptionShouldThrowAgainWhenNotHandled(): void { + $m1 = new TestMiddleware(false); + $m2 = new TestMiddleware(true); + + $this->dispatcher->registerMiddleware($m1); + $this->dispatcher->registerMiddleware($m2); + + $this->expectException(\Exception::class); + $this->dispatcher->beforeController($this->controller, $this->method); + $this->dispatcher->afterException($this->controller, $this->method, $this->exception); + } + + + public function testBeforeControllerCorrectArguments(): void { + $m1 = $this->getMiddleware(); + $this->dispatcher->beforeController($this->controller, $this->method); + + $this->assertEquals($this->controller, $m1->controller); + $this->assertEquals($this->method, $m1->methodName); + } + + + public function testAfterControllerCorrectArguments(): void { + $m1 = $this->getMiddleware(); + + $this->dispatcher->afterController($this->controller, $this->method, $this->response); + + $this->assertEquals($this->controller, $m1->controller); + $this->assertEquals($this->method, $m1->methodName); + $this->assertEquals($this->response, $m1->response); + } + + + public function testAfterExceptionCorrectArguments(): void { + $m1 = $this->getMiddleware(); + + $this->expectException(\Exception::class); + + $this->dispatcher->beforeController($this->controller, $this->method); + $this->dispatcher->afterException($this->controller, $this->method, $this->exception); + + $this->assertEquals($this->controller, $m1->controller); + $this->assertEquals($this->method, $m1->methodName); + $this->assertEquals($this->exception, $m1->exception); + } + + + public function testBeforeOutputCorrectArguments(): void { + $m1 = $this->getMiddleware(); + + $this->dispatcher->beforeOutput($this->controller, $this->method, $this->out); + + $this->assertEquals($this->controller, $m1->controller); + $this->assertEquals($this->method, $m1->methodName); + $this->assertEquals($this->out, $m1->output); + } + + + public function testBeforeControllerOrder(): void { + $m1 = $this->getMiddleware(); + $m2 = $this->getMiddleware(); + + $this->dispatcher->beforeController($this->controller, $this->method); + + $this->assertEquals(1, $m1->beforeControllerOrder); + $this->assertEquals(2, $m2->beforeControllerOrder); + } + + public function testAfterControllerOrder(): void { + $m1 = $this->getMiddleware(); + $m2 = $this->getMiddleware(); + + $this->dispatcher->afterController($this->controller, $this->method, $this->response); + + $this->assertEquals(2, $m1->afterControllerOrder); + $this->assertEquals(1, $m2->afterControllerOrder); + } + + + public function testAfterExceptionOrder(): void { + $m1 = $this->getMiddleware(); + $m2 = $this->getMiddleware(); + + $this->expectException(\Exception::class); + $this->dispatcher->beforeController($this->controller, $this->method); + $this->dispatcher->afterException($this->controller, $this->method, $this->exception); + + $this->assertEquals(1, $m1->afterExceptionOrder); + $this->assertEquals(1, $m2->afterExceptionOrder); + } + + + public function testBeforeOutputOrder(): void { + $m1 = $this->getMiddleware(); + $m2 = $this->getMiddleware(); + + $this->dispatcher->beforeOutput($this->controller, $this->method, $this->out); + + $this->assertEquals(2, $m1->beforeOutputOrder); + $this->assertEquals(1, $m2->beforeOutputOrder); + } + + + public function testExceptionShouldRunAfterExceptionOfOnlyPreviouslyExecutedMiddlewares(): void { + $m1 = $this->getMiddleware(); + $m2 = $this->getMiddleware(true); + $m3 = $this->createMock(Middleware::class); + $m3->expects($this->never()) + ->method('afterException'); + $m3->expects($this->never()) + ->method('beforeController'); + $m3->expects($this->never()) + ->method('afterController'); + $m3->method('beforeOutput') + ->willReturnArgument(2); + + $this->dispatcher->registerMiddleware($m3); + + $this->dispatcher->beforeOutput($this->controller, $this->method, $this->out); + + $this->assertEquals(2, $m1->beforeOutputOrder); + $this->assertEquals(1, $m2->beforeOutputOrder); + } +} diff --git a/tests/lib/AppFramework/Middleware/MiddlewareTest.php b/tests/lib/AppFramework/Middleware/MiddlewareTest.php new file mode 100644 index 00000000000..addd9683122 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/MiddlewareTest.php @@ -0,0 +1,79 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\DependencyInjection\DIContainer; +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\IConfig; +use OCP\IRequestId; + +class ChildMiddleware extends Middleware { +}; + + +class MiddlewareTest extends \Test\TestCase { + /** + * @var Middleware + */ + private $middleware; + private $controller; + private $exception; + private $api; + /** @var Response */ + private $response; + + protected function setUp(): void { + parent::setUp(); + + $this->middleware = new ChildMiddleware(); + + $this->api = $this->createMock(DIContainer::class); + + $this->controller = $this->getMockBuilder(Controller::class) + ->setConstructorArgs([ + $this->api, + new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ) + ])->getMock(); + $this->exception = new \Exception(); + $this->response = $this->createMock(Response::class); + } + + + public function testBeforeController(): void { + $this->middleware->beforeController($this->controller, ''); + $this->assertNull(null); + } + + + public function testAfterExceptionRaiseAgainWhenUnhandled(): void { + $this->expectException(\Exception::class); + $this->middleware->afterException($this->controller, '', $this->exception); + } + + + public function testAfterControllerReturnResponseWhenUnhandled(): void { + $response = $this->middleware->afterController($this->controller, '', $this->response); + + $this->assertEquals($this->response, $response); + } + + + public function testBeforeOutputReturnOutputhenUnhandled(): void { + $output = $this->middleware->beforeOutput($this->controller, '', 'test'); + + $this->assertEquals('test', $output); + } +} diff --git a/tests/lib/AppFramework/Middleware/NotModifiedMiddlewareTest.php b/tests/lib/AppFramework/Middleware/NotModifiedMiddlewareTest.php new file mode 100644 index 00000000000..7dcb28a2af4 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/NotModifiedMiddlewareTest.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\Middleware\NotModifiedMiddleware; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; + +class NotModifiedMiddlewareTest extends \Test\TestCase { + /** @var IRequest */ + private $request; + /** @var Controller */ + private $controller; + /** @var NotModifiedMiddleware */ + private $middleWare; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->middleWare = new NotModifiedMiddleware( + $this->request + ); + + $this->controller = $this->createMock(Controller::class); + } + + public static function dataModified(): array { + $now = new \DateTime(); + + return [ + [null, '', null, '', false], + ['etag', 'etag', null, '', false], + ['etag', '"wrongetag"', null, '', false], + ['etag', '', null, '', false], + [null, '"etag"', null, '', false], + ['etag', '"etag"', null, '', true], + + [null, '', $now, $now->format(\DateTimeInterface::RFC7231), true], + [null, '', $now, $now->format(\DateTimeInterface::ATOM), false], + [null, '', null, $now->format(\DateTimeInterface::RFC7231), false], + [null, '', $now, '', false], + + ['etag', '"etag"', $now, $now->format(\DateTimeInterface::ATOM), true], + ['etag', '"etag"', $now, $now->format(\DateTimeInterface::RFC7231), true], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataModified')] + public function testMiddleware(?string $etag, string $etagHeader, ?\DateTime $lastModified, string $lastModifiedHeader, bool $notModifiedSet): void { + $this->request->method('getHeader') + ->willReturnCallback(function (string $name) use ($etagHeader, $lastModifiedHeader) { + if ($name === 'IF_NONE_MATCH') { + return $etagHeader; + } + if ($name === 'IF_MODIFIED_SINCE') { + return $lastModifiedHeader; + } + return ''; + }); + + $response = new Response(); + if ($etag !== null) { + $response->setETag($etag); + } + if ($lastModified !== null) { + $response->setLastModified($lastModified); + } + $response->setStatus(Http::STATUS_OK); + + $result = $this->middleWare->afterController($this->controller, 'myfunction', $response); + + if ($notModifiedSet) { + $this->assertSame(Http::STATUS_NOT_MODIFIED, $result->getStatus()); + } else { + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + } + } +} diff --git a/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php new file mode 100644 index 00000000000..e5c6a417a4b --- /dev/null +++ b/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php @@ -0,0 +1,184 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\Middleware\OCSMiddleware; +use OC\AppFramework\OCS\BaseResponse; +use OC\AppFramework\OCS\V1Response; +use OC\AppFramework\OCS\V2Response; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +class OCSMiddlewareTest extends \Test\TestCase { + /** + * @var IRequest + */ + private $request; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->getMockBuilder(IRequest::class) + ->getMock(); + } + + public static function dataAfterException(): array { + return [ + [OCSController::class, new \Exception(), true], + [OCSController::class, new OCSException(), false, '', Http::STATUS_INTERNAL_SERVER_ERROR], + [OCSController::class, new OCSException('foo'), false, 'foo', Http::STATUS_INTERNAL_SERVER_ERROR], + [OCSController::class, new OCSException('foo', Http::STATUS_IM_A_TEAPOT), false, 'foo', Http::STATUS_IM_A_TEAPOT], + [OCSController::class, new OCSBadRequestException(), false, '', Http::STATUS_BAD_REQUEST], + [OCSController::class, new OCSBadRequestException('foo'), false, 'foo', Http::STATUS_BAD_REQUEST], + [OCSController::class, new OCSForbiddenException(), false, '', Http::STATUS_FORBIDDEN], + [OCSController::class, new OCSForbiddenException('foo'), false, 'foo', Http::STATUS_FORBIDDEN], + [OCSController::class, new OCSNotFoundException(), false, '', Http::STATUS_NOT_FOUND], + [OCSController::class, new OCSNotFoundException('foo'), false, 'foo', Http::STATUS_NOT_FOUND], + + [Controller::class, new \Exception(), true], + [Controller::class, new OCSException(), true], + [Controller::class, new OCSException('foo'), true], + [Controller::class, new OCSException('foo', Http::STATUS_IM_A_TEAPOT), true], + [Controller::class, new OCSBadRequestException(), true], + [Controller::class, new OCSBadRequestException('foo'), true], + [Controller::class, new OCSForbiddenException(), true], + [Controller::class, new OCSForbiddenException('foo'), true], + [Controller::class, new OCSNotFoundException(), true], + [Controller::class, new OCSNotFoundException('foo'), true], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterException')] + public function testAfterExceptionOCSv1(string $controller, \Exception $exception, bool $forward, string $message = '', int $code = 0): void { + $controller = $this->createMock($controller); + $this->request + ->method('getScriptName') + ->willReturn('/ocs/v1.php'); + $OCSMiddleware = new OCSMiddleware($this->request); + $OCSMiddleware->beforeController($controller, 'method'); + + if ($forward) { + $this->expectException(get_class($exception)); + $this->expectExceptionMessage($exception->getMessage()); + } + + $result = $OCSMiddleware->afterException($controller, 'method', $exception); + + $this->assertInstanceOf(V1Response::class, $result); + + $this->assertSame($message, $this->invokePrivate($result, 'statusMessage')); + + if ($exception->getCode() === 0) { + $this->assertSame(OCSController::RESPOND_UNKNOWN_ERROR, $result->getOCSStatus()); + } else { + $this->assertSame($code, $result->getOCSStatus()); + } + + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterException')] + public function testAfterExceptionOCSv2(string $controller, \Exception $exception, bool $forward, string $message = '', int $code = 0): void { + $controller = $this->createMock($controller); + $this->request + ->method('getScriptName') + ->willReturn('/ocs/v2.php'); + $OCSMiddleware = new OCSMiddleware($this->request); + $OCSMiddleware->beforeController($controller, 'method'); + + if ($forward) { + $this->expectException(get_class($exception)); + $this->expectExceptionMessage($exception->getMessage()); + } + + $result = $OCSMiddleware->afterException($controller, 'method', $exception); + + $this->assertInstanceOf(V2Response::class, $result); + + $this->assertSame($message, $this->invokePrivate($result, 'statusMessage')); + if ($exception->getCode() === 0) { + $this->assertSame(OCSController::RESPOND_UNKNOWN_ERROR, $result->getOCSStatus()); + } else { + $this->assertSame($code, $result->getOCSStatus()); + } + $this->assertSame($code, $result->getStatus()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterException')] + public function testAfterExceptionOCSv2SubFolder(string $controller, \Exception $exception, bool $forward, string $message = '', int $code = 0): void { + $controller = $this->createMock($controller); + $this->request + ->method('getScriptName') + ->willReturn('/mysubfolder/ocs/v2.php'); + $OCSMiddleware = new OCSMiddleware($this->request); + $OCSMiddleware->beforeController($controller, 'method'); + + if ($forward) { + $this->expectException($exception::class); + $this->expectExceptionMessage($exception->getMessage()); + } + + $result = $OCSMiddleware->afterException($controller, 'method', $exception); + + $this->assertInstanceOf(V2Response::class, $result); + + $this->assertSame($message, $this->invokePrivate($result, 'statusMessage')); + if ($exception->getCode() === 0) { + $this->assertSame(OCSController::RESPOND_UNKNOWN_ERROR, $result->getOCSStatus()); + } else { + $this->assertSame($code, $result->getOCSStatus()); + } + $this->assertSame($code, $result->getStatus()); + } + + public static function dataAfterController(): array { + return [ + [OCSController::class, new Response(), false], + [OCSController::class, new JSONResponse(), false], + [OCSController::class, new JSONResponse(['message' => 'foo']), false], + [OCSController::class, new JSONResponse(['message' => 'foo'], Http::STATUS_UNAUTHORIZED), true, OCSController::RESPOND_UNAUTHORISED], + [OCSController::class, new JSONResponse(['message' => 'foo'], Http::STATUS_FORBIDDEN), true], + + [Controller::class, new Response(), false], + [Controller::class, new JSONResponse(), false], + [Controller::class, new JSONResponse(['message' => 'foo']), false], + [Controller::class, new JSONResponse(['message' => 'foo'], Http::STATUS_UNAUTHORIZED), false], + [Controller::class, new JSONResponse(['message' => 'foo'], Http::STATUS_FORBIDDEN), false], + + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterController')] + public function testAfterController(string $controller, Response $response, bool $converted, int $convertedOCSStatus = 0): void { + $controller = $this->createMock($controller); + $OCSMiddleware = new OCSMiddleware($this->request); + $newResponse = $OCSMiddleware->afterController($controller, 'foo', $response); + + if ($converted === false) { + $this->assertSame($response, $newResponse); + } else { + $this->assertInstanceOf(BaseResponse::class, $newResponse); + $this->assertSame($response->getData()['message'], $this->invokePrivate($newResponse, 'statusMessage')); + + if ($convertedOCSStatus) { + $this->assertSame($convertedOCSStatus, $newResponse->getOCSStatus()); + } else { + $this->assertSame($response->getStatus(), $newResponse->getOCSStatus()); + } + $this->assertSame($response->getStatus(), $newResponse->getStatus()); + } + } +} diff --git a/tests/lib/AppFramework/Middleware/PublicShare/PublicShareMiddlewareTest.php b/tests/lib/AppFramework/Middleware/PublicShare/PublicShareMiddlewareTest.php new file mode 100644 index 00000000000..e87ee7fd565 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/PublicShare/PublicShareMiddlewareTest.php @@ -0,0 +1,273 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\PublicShare; + +use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException; +use OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware; +use OCP\AppFramework\AuthPublicShareController; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\PublicShareController; +use OCP\Files\NotFoundException; +use OCP\IConfig; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\Security\Bruteforce\IThrottler; + +class PublicShareMiddlewareTest extends \Test\TestCase { + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */ + private $session; + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + private $config; + /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ + private $throttler; + + /** @var PublicShareMiddleware */ + private $middleware; + + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->session = $this->createMock(ISession::class); + $this->config = $this->createMock(IConfig::class); + $this->throttler = $this->createMock(IThrottler::class); + + $this->middleware = new PublicShareMiddleware( + $this->request, + $this->session, + $this->config, + $this->throttler + ); + } + + public function testBeforeControllerNoPublicShareController(): void { + $controller = $this->createMock(Controller::class); + + $this->middleware->beforeController($controller, 'method'); + $this->assertTrue(true); + } + + public static function dataShareApi(): array { + return [ + ['no', 'no',], + ['no', 'yes',], + ['yes', 'no',], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataShareApi')] + public function testBeforeControllerShareApiDisabled(string $shareApi, string $shareLinks): void { + $controller = $this->createMock(PublicShareController::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', $shareApi], + ['core', 'shareapi_allow_links', 'yes', $shareLinks], + ]); + + $this->expectException(NotFoundException::class); + $this->middleware->beforeController($controller, 'mehod'); + } + + public function testBeforeControllerNoTokenParam(): void { + $controller = $this->createMock(PublicShareController::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ]); + + $this->expectException(NotFoundException::class); + $this->middleware->beforeController($controller, 'mehod'); + } + + public function testBeforeControllerInvalidToken(): void { + $controller = $this->createMock(PublicShareController::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ]); + + $this->request->method('getParam') + ->with('token', null) + ->willReturn('myToken'); + + $controller->method('isValidToken') + ->willReturn(false); + $controller->expects($this->once()) + ->method('shareNotFound'); + + $this->expectException(NotFoundException::class); + $this->middleware->beforeController($controller, 'mehod'); + } + + public function testBeforeControllerValidTokenNotAuthenticated(): void { + $controller = $this->getMockBuilder(PublicShareController::class) + ->setConstructorArgs(['app', $this->request, $this->session]) + ->getMock(); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ]); + + $this->request->method('getParam') + ->with('token', null) + ->willReturn('myToken'); + + $controller->method('isValidToken') + ->willReturn(true); + + $controller->method('isPasswordProtected') + ->willReturn(true); + + $this->expectException(NotFoundException::class); + $this->middleware->beforeController($controller, 'mehod'); + } + + public function testBeforeControllerValidTokenAuthenticateMethod(): void { + $controller = $this->getMockBuilder(PublicShareController::class) + ->setConstructorArgs(['app', $this->request, $this->session]) + ->getMock(); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ]); + + $this->request->method('getParam') + ->with('token', null) + ->willReturn('myToken'); + + $controller->method('isValidToken') + ->willReturn(true); + + $controller->method('isPasswordProtected') + ->willReturn(true); + + $this->middleware->beforeController($controller, 'authenticate'); + $this->assertTrue(true); + } + + public function testBeforeControllerValidTokenShowAuthenticateMethod(): void { + $controller = $this->getMockBuilder(PublicShareController::class) + ->setConstructorArgs(['app', $this->request, $this->session]) + ->getMock(); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ]); + + $this->request->method('getParam') + ->with('token', null) + ->willReturn('myToken'); + + $controller->method('isValidToken') + ->willReturn(true); + + $controller->method('isPasswordProtected') + ->willReturn(true); + + $this->middleware->beforeController($controller, 'showAuthenticate'); + $this->assertTrue(true); + } + + public function testBeforeControllerAuthPublicShareController(): void { + $controller = $this->getMockBuilder(AuthPublicShareController::class) + ->setConstructorArgs(['app', $this->request, $this->session, $this->createMock(IURLGenerator::class)]) + ->getMock(); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ]); + + $this->request->method('getParam') + ->with('token', null) + ->willReturn('myToken'); + + $controller->method('isValidToken') + ->willReturn(true); + + $controller->method('isPasswordProtected') + ->willReturn(true); + + $this->session->expects($this->once()) + ->method('set') + ->with('public_link_authenticate_redirect', '[]'); + + $this->expectException(NeedAuthenticationException::class); + $this->middleware->beforeController($controller, 'method'); + } + + public function testAfterExceptionNoPublicShareController(): void { + $controller = $this->createMock(Controller::class); + $exception = new \Exception(); + + try { + $this->middleware->afterException($controller, 'method', $exception); + } catch (\Exception $e) { + $this->assertEquals($exception, $e); + } + } + + public function testAfterExceptionPublicShareControllerNotFoundException(): void { + $controller = $this->createMock(PublicShareController::class); + $exception = new NotFoundException(); + + $result = $this->middleware->afterException($controller, 'method', $exception); + $this->assertInstanceOf(TemplateResponse::class, $result); + $this->assertEquals($result->getStatus(), Http::STATUS_NOT_FOUND); + } + + public function testAfterExceptionPublicShareController(): void { + $controller = $this->createMock(PublicShareController::class); + $exception = new \Exception(); + + try { + $this->middleware->afterException($controller, 'method', $exception); + } catch (\Exception $e) { + $this->assertEquals($exception, $e); + } + } + + public function testAfterExceptionAuthPublicShareController(): void { + $controller = $this->getMockBuilder(AuthPublicShareController::class) + ->setConstructorArgs([ + 'app', + $this->request, + $this->session, + $this->createMock(IURLGenerator::class), + ])->getMock(); + $controller->setToken('token'); + + $exception = new NeedAuthenticationException(); + + $this->request->method('getParam') + ->with('_route') + ->willReturn('my.route'); + + $result = $this->middleware->afterException($controller, 'method', $exception); + $this->assertInstanceOf(RedirectResponse::class, $result); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php new file mode 100644 index 00000000000..3fd2cb38a33 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php @@ -0,0 +1,328 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\BruteForceMiddleware; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; +use OCP\Security\Bruteforce\IThrottler; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class TestController extends Controller { + /** + * @BruteForceProtection(action=login) + */ + public function testMethodWithAnnotation() { + } + + public function testMethodWithoutAnnotation() { + } + + #[BruteForceProtection(action: 'single')] + public function singleAttribute(): void { + } + + #[BruteForceProtection(action: 'first')] + #[BruteForceProtection(action: 'second')] + public function multipleAttributes(): void { + } +} + +class BruteForceMiddlewareTest extends TestCase { + /** @var ControllerMethodReflector */ + private $reflector; + /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ + private $throttler; + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + private BruteForceMiddleware $bruteForceMiddleware; + + protected function setUp(): void { + parent::setUp(); + + $this->reflector = new ControllerMethodReflector(); + $this->throttler = $this->createMock(IThrottler::class); + $this->request = $this->createMock(IRequest::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->bruteForceMiddleware = new BruteForceMiddleware( + $this->reflector, + $this->throttler, + $this->request, + $this->logger, + ); + } + + public function testBeforeControllerWithAnnotation(): void { + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('127.0.0.1', 'login'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithAnnotation'); + } + + public function testBeforeControllerWithSingleAttribute(): void { + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('::1', 'single'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'singleAttribute'); + $this->bruteForceMiddleware->beforeController($controller, 'singleAttribute'); + } + + public function testBeforeControllerWithMultipleAttributes(): void { + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + + $calls = [ + ['::1', 'first'], + ['::1', 'second'], + ]; + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelayOrThrowOnMax') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return 0; + }); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'multipleAttributes'); + $this->bruteForceMiddleware->beforeController($controller, 'multipleAttributes'); + } + + public function testBeforeControllerWithoutAnnotation(): void { + $this->request + ->expects($this->never()) + ->method('getRemoteAddress'); + $this->throttler + ->expects($this->never()) + ->method('sleepDelayOrThrowOnMax'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); + } + + public function testAfterControllerWithAnnotationAndThrottledRequest(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn([]); + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('127.0.0.1', 'login'); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('login', '127.0.0.1'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response); + } + + public function testAfterControllerWithAnnotationAndNotThrottledRequest(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(false); + $this->request + ->expects($this->never()) + ->method('getRemoteAddress'); + $this->throttler + ->expects($this->never()) + ->method('sleepDelayOrThrowOnMax'); + $this->throttler + ->expects($this->never()) + ->method('registerAttempt'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response); + } + + public function testAfterControllerWithSingleAttribute(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn([]); + + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('::1', 'single'); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('single', '::1'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'singleAttribute'); + $this->bruteForceMiddleware->afterController($controller, 'singleAttribute', $response); + } + + public function testAfterControllerWithMultipleAttributesGeneralMatch(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn([]); + + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + + $sleepCalls = [ + ['::1', 'first'], + ['::1', 'second'], + ]; + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelayOrThrowOnMax') + ->willReturnCallback(function () use (&$sleepCalls) { + $expected = array_shift($sleepCalls); + $this->assertEquals($expected, func_get_args()); + return 0; + }); + + $attemptCalls = [ + ['first', '::1', []], + ['second', '::1', []], + ]; + $this->throttler + ->expects($this->exactly(2)) + ->method('registerAttempt') + ->willReturnCallback(function () use (&$attemptCalls): void { + $expected = array_shift($attemptCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'multipleAttributes'); + $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response); + } + + public function testAfterControllerWithMultipleAttributesSpecificMatch(): void { + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response + ->expects($this->once()) + ->method('isThrottled') + ->willReturn(true); + $response + ->expects($this->once()) + ->method('getThrottleMetadata') + ->willReturn(['action' => 'second']); + + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('::1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('::1', 'second'); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('second', '::1'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'multipleAttributes'); + $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response); + } + + public function testAfterControllerWithoutAnnotation(): void { + $this->request + ->expects($this->never()) + ->method('getRemoteAddress'); + $this->throttler + ->expects($this->never()) + ->method('sleepDelayOrThrowOnMax'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response); + } + + public function testAfterControllerWithThrottledResponseButUnhandled(): void { + $this->request + ->expects($this->never()) + ->method('getRemoteAddress'); + $this->throttler + ->expects($this->never()) + ->method('sleepDelayOrThrowOnMax'); + + $controller = new TestController('test', $this->request); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ + $response = $this->createMock(Response::class); + $response->method('isThrottled') + ->willReturn(true); + + $this->logger->expects($this->once()) + ->method('debug') + ->with('Response for Test\AppFramework\Middleware\Security\TestController::testMethodWithoutAnnotation got bruteforce throttled but has no annotation nor attribute defined.'); + + $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php new file mode 100644 index 00000000000..c325ae638fb --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php @@ -0,0 +1,344 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Http\Request; +use OC\AppFramework\Middleware\Security\CORSMiddleware; +use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Authentication\Exceptions\PasswordLoginForbiddenException; +use OC\User\Session; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IRequestId; +use OCP\Security\Bruteforce\IThrottler; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController; + +class CORSMiddlewareTest extends \Test\TestCase { + /** @var ControllerMethodReflector */ + private $reflector; + /** @var Session|MockObject */ + private $session; + /** @var IThrottler|MockObject */ + private $throttler; + /** @var CORSMiddlewareController */ + private $controller; + private LoggerInterface $logger; + + protected function setUp(): void { + parent::setUp(); + $this->reflector = new ControllerMethodReflector(); + $this->session = $this->createMock(Session::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->controller = new CORSMiddlewareController( + 'test', + $this->createMock(IRequest::class) + ); + } + + public static function dataSetCORSAPIHeader(): array { + return [ + ['testSetCORSAPIHeader'], + ['testSetCORSAPIHeaderAttribute'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSetCORSAPIHeader')] + public function testSetCORSAPIHeader(string $method): void { + $request = new Request( + [ + 'server' => [ + 'HTTP_ORIGIN' => 'test' + ] + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $response = $middleware->afterController($this->controller, $method, new Response()); + $headers = $response->getHeaders(); + $this->assertEquals('test', $headers['Access-Control-Allow-Origin']); + } + + public function testNoAnnotationNoCORSHEADER(): void { + $request = new Request( + [ + 'server' => [ + 'HTTP_ORIGIN' => 'test' + ] + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $response = $middleware->afterController($this->controller, __FUNCTION__, new Response()); + $headers = $response->getHeaders(); + $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); + } + + public static function dataNoOriginHeaderNoCORSHEADER(): array { + return [ + ['testNoOriginHeaderNoCORSHEADER'], + ['testNoOriginHeaderNoCORSHEADERAttribute'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOriginHeaderNoCORSHEADER')] + public function testNoOriginHeaderNoCORSHEADER(string $method): void { + $request = new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $response = $middleware->afterController($this->controller, $method, new Response()); + $headers = $response->getHeaders(); + $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); + } + + public static function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array { + return [ + ['testCorsIgnoredIfWithCredentialsHeaderPresent'], + ['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCorsIgnoredIfWithCredentialsHeaderPresent')] + public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void { + $this->expectException(SecurityException::class); + + $request = new Request( + [ + 'server' => [ + 'HTTP_ORIGIN' => 'test' + ] + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $response = new Response(); + $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE'); + $middleware->afterController($this->controller, $method, $response); + } + + public static function dataNoCORSOnAnonymousPublicPage(): array { + return [ + ['testNoCORSOnAnonymousPublicPage'], + ['testNoCORSOnAnonymousPublicPageAttribute'], + ['testNoCORSAttributeOnAnonymousPublicPage'], + ['testNoCORSAttributeOnAnonymousPublicPageAttribute'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCORSOnAnonymousPublicPage')] + public function testNoCORSOnAnonymousPublicPage(string $method): void { + $request = new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $this->session->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->session->expects($this->never()) + ->method('logout'); + $this->session->expects($this->never()) + ->method('logClientIn') + ->with($this->equalTo('user'), $this->equalTo('pass')) + ->willReturn(true); + $this->reflector->reflect($this->controller, $method); + + $middleware->beforeController($this->controller, $method); + } + + public static function dataCORSShouldNeverAllowCookieAuth(): array { + return [ + ['testCORSShouldNeverAllowCookieAuth'], + ['testCORSShouldNeverAllowCookieAuthAttribute'], + ['testCORSAttributeShouldNeverAllowCookieAuth'], + ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNeverAllowCookieAuth')] + public function testCORSShouldNeverAllowCookieAuth(string $method): void { + $request = new Request( + [], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $this->session->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session->expects($this->once()) + ->method('logout'); + $this->session->expects($this->never()) + ->method('logClientIn') + ->with($this->equalTo('user'), $this->equalTo('pass')) + ->willReturn(true); + + $this->expectException(SecurityException::class); + $middleware->beforeController($this->controller, $method); + } + + public static function dataCORSShouldRelogin(): array { + return [ + ['testCORSShouldRelogin'], + ['testCORSAttributeShouldRelogin'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldRelogin')] + public function testCORSShouldRelogin(string $method): void { + $request = new Request( + ['server' => [ + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass' + ]], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->session->expects($this->once()) + ->method('logout'); + $this->session->expects($this->once()) + ->method('logClientIn') + ->with($this->equalTo('user'), $this->equalTo('pass')) + ->willReturn(true); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $middleware->beforeController($this->controller, $method); + } + + public static function dataCORSShouldFailIfPasswordLoginIsForbidden(): array { + return [ + ['testCORSShouldFailIfPasswordLoginIsForbidden'], + ['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldFailIfPasswordLoginIsForbidden')] + public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void { + $this->expectException(SecurityException::class); + + $request = new Request( + ['server' => [ + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass' + ]], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->session->expects($this->once()) + ->method('logout'); + $this->session->expects($this->once()) + ->method('logClientIn') + ->with($this->equalTo('user'), $this->equalTo('pass')) + ->willThrowException(new PasswordLoginForbiddenException); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $middleware->beforeController($this->controller, $method); + } + + public static function dataCORSShouldNotAllowCookieAuth(): array { + return [ + ['testCORSShouldNotAllowCookieAuth'], + ['testCORSAttributeShouldNotAllowCookieAuth'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNotAllowCookieAuth')] + public function testCORSShouldNotAllowCookieAuth(string $method): void { + $this->expectException(SecurityException::class); + + $request = new Request( + ['server' => [ + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass' + ]], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->session->expects($this->once()) + ->method('logout'); + $this->session->expects($this->once()) + ->method('logClientIn') + ->with($this->equalTo('user'), $this->equalTo('pass')) + ->willReturn(false); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + + $middleware->beforeController($this->controller, $method); + } + + public function testAfterExceptionWithSecurityExceptionNoStatus(): void { + $request = new Request( + ['server' => [ + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass' + ]], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception')); + + $expected = new JSONResponse(['message' => 'A security exception'], 500); + $this->assertEquals($expected, $response); + } + + public function testAfterExceptionWithSecurityExceptionWithStatus(): void { + $request = new Request( + ['server' => [ + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass' + ]], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501)); + + $expected = new JSONResponse(['message' => 'A security exception'], 501); + $this->assertEquals($expected, $response); + } + + public function testAfterExceptionWithRegularException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('A regular exception'); + + $request = new Request( + ['server' => [ + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass' + ]], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception')); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php new file mode 100644 index 00000000000..b0b41b27cb9 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php @@ -0,0 +1,122 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\CSPMiddleware; +use OC\Security\CSP\ContentSecurityPolicy; +use OC\Security\CSP\ContentSecurityPolicyManager; +use OC\Security\CSP\ContentSecurityPolicyNonceManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; +use OCP\AppFramework\Http\Response; +use PHPUnit\Framework\MockObject\MockObject; + +class CSPMiddlewareTest extends \Test\TestCase { + /** @var CSPMiddleware&MockObject */ + private $middleware; + /** @var Controller&MockObject */ + private $controller; + /** @var ContentSecurityPolicyManager&MockObject */ + private $contentSecurityPolicyManager; + /** @var ContentSecurityPolicyNonceManager&MockObject */ + private $cspNonceManager; + + protected function setUp(): void { + parent::setUp(); + + $this->controller = $this->createMock(Controller::class); + $this->contentSecurityPolicyManager = $this->createMock(ContentSecurityPolicyManager::class); + $this->cspNonceManager = $this->createMock(ContentSecurityPolicyNonceManager::class); + $this->middleware = new CSPMiddleware( + $this->contentSecurityPolicyManager, + $this->cspNonceManager, + ); + } + + public function testAfterController(): void { + $this->cspNonceManager + ->expects($this->once()) + ->method('browserSupportsCspV3') + ->willReturn(false); + $response = $this->createMock(Response::class); + $defaultPolicy = new ContentSecurityPolicy(); + $defaultPolicy->addAllowedImageDomain('defaultpolicy'); + $currentPolicy = new ContentSecurityPolicy(); + $currentPolicy->addAllowedConnectDomain('currentPolicy'); + $mergedPolicy = new ContentSecurityPolicy(); + $mergedPolicy->addAllowedMediaDomain('mergedPolicy'); + $response + ->expects($this->exactly(2)) + ->method('getContentSecurityPolicy') + ->willReturn($currentPolicy); + $this->contentSecurityPolicyManager + ->expects($this->once()) + ->method('getDefaultPolicy') + ->willReturn($defaultPolicy); + $this->contentSecurityPolicyManager + ->expects($this->once()) + ->method('mergePolicies') + ->with($defaultPolicy, $currentPolicy) + ->willReturn($mergedPolicy); + $response->expects($this->once()) + ->method('setContentSecurityPolicy') + ->with($mergedPolicy); + + $this->middleware->afterController($this->controller, 'test', $response); + } + + public function testAfterControllerEmptyCSP(): void { + $response = $this->createMock(Response::class); + $emptyPolicy = new EmptyContentSecurityPolicy(); + $response->expects($this->any()) + ->method('getContentSecurityPolicy') + ->willReturn($emptyPolicy); + $response->expects($this->never()) + ->method('setContentSecurityPolicy'); + + $this->middleware->afterController($this->controller, 'test', $response); + } + + public function testAfterControllerWithContentSecurityPolicy3Support(): void { + $this->cspNonceManager + ->expects($this->once()) + ->method('browserSupportsCspV3') + ->willReturn(true); + $token = base64_encode('the-nonce'); + $this->cspNonceManager + ->expects($this->once()) + ->method('getNonce') + ->willReturn($token); + $response = $this->createMock(Response::class); + $defaultPolicy = new ContentSecurityPolicy(); + $defaultPolicy->addAllowedImageDomain('defaultpolicy'); + $currentPolicy = new ContentSecurityPolicy(); + $currentPolicy->addAllowedConnectDomain('currentPolicy'); + $mergedPolicy = new ContentSecurityPolicy(); + $mergedPolicy->addAllowedMediaDomain('mergedPolicy'); + $response + ->expects($this->exactly(2)) + ->method('getContentSecurityPolicy') + ->willReturn($currentPolicy); + $this->contentSecurityPolicyManager + ->expects($this->once()) + ->method('getDefaultPolicy') + ->willReturn($defaultPolicy); + $this->contentSecurityPolicyManager + ->expects($this->once()) + ->method('mergePolicies') + ->with($defaultPolicy, $currentPolicy) + ->willReturn($mergedPolicy); + $response->expects($this->once()) + ->method('setContentSecurityPolicy') + ->with($mergedPolicy); + + $this->assertEquals($response, $this->middleware->afterController($this->controller, 'test', $response)); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php new file mode 100644 index 00000000000..55a70d4c040 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware; +use OC\Security\FeaturePolicy\FeaturePolicy; +use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\AppFramework\Http\Response; +use PHPUnit\Framework\MockObject\MockObject; + +class FeaturePolicyMiddlewareTest extends \Test\TestCase { + /** @var FeaturePolicyMiddleware|MockObject */ + private $middleware; + /** @var Controller|MockObject */ + private $controller; + /** @var FeaturePolicyManager|MockObject */ + private $manager; + + protected function setUp(): void { + parent::setUp(); + + $this->controller = $this->createMock(Controller::class); + $this->manager = $this->createMock(FeaturePolicyManager::class); + $this->middleware = new FeaturePolicyMiddleware( + $this->manager + ); + } + + public function testAfterController(): void { + $response = $this->createMock(Response::class); + $defaultPolicy = new FeaturePolicy(); + $defaultPolicy->addAllowedCameraDomain('defaultpolicy'); + $currentPolicy = new FeaturePolicy(); + $currentPolicy->addAllowedAutoplayDomain('currentPolicy'); + $mergedPolicy = new FeaturePolicy(); + $mergedPolicy->addAllowedGeoLocationDomain('mergedPolicy'); + $response->method('getFeaturePolicy') + ->willReturn($currentPolicy); + $this->manager->method('getDefaultPolicy') + ->willReturn($defaultPolicy); + $this->manager->method('mergePolicies') + ->with($defaultPolicy, $currentPolicy) + ->willReturn($mergedPolicy); + $response->expects($this->once()) + ->method('setFeaturePolicy') + ->with($mergedPolicy); + + $this->middleware->afterController($this->controller, 'test', $response); + } + + public function testAfterControllerEmptyCSP(): void { + $response = $this->createMock(Response::class); + $emptyPolicy = new EmptyFeaturePolicy(); + $response->method('getFeaturePolicy') + ->willReturn($emptyPolicy); + $response->expects($this->never()) + ->method('setFeaturePolicy'); + + $this->middleware->afterController($this->controller, 'test', $response); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php new file mode 100644 index 00000000000..8ab3a48b62e --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php @@ -0,0 +1,145 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\CORS; +use OCP\AppFramework\Http\Attribute\PublicPage; + +class CORSMiddlewareController extends Controller { + /** + * @CORS + */ + public function testSetCORSAPIHeader() { + } + + #[CORS] + public function testSetCORSAPIHeaderAttribute() { + } + + public function testNoAnnotationNoCORSHEADER() { + } + + /** + * @CORS + */ + public function testNoOriginHeaderNoCORSHEADER() { + } + + #[CORS] + public function testNoOriginHeaderNoCORSHEADERAttribute() { + } + + /** + * @CORS + */ + public function testCorsIgnoredIfWithCredentialsHeaderPresent() { + } + + #[CORS] + public function testCorsAttributeIgnoredIfWithCredentialsHeaderPresent() { + } + + /** + * CORS must not be enforced for anonymous users on public pages + * + * @CORS + * @PublicPage + */ + public function testNoCORSOnAnonymousPublicPage() { + } + + /** + * CORS must not be enforced for anonymous users on public pages + * + * @CORS + */ + #[PublicPage] + public function testNoCORSOnAnonymousPublicPageAttribute() { + } + + /** + * @PublicPage + */ + #[CORS] + public function testNoCORSAttributeOnAnonymousPublicPage() { + } + + #[CORS] + #[PublicPage] + public function testNoCORSAttributeOnAnonymousPublicPageAttribute() { + } + + /** + * @CORS + * @PublicPage + */ + public function testCORSShouldNeverAllowCookieAuth() { + } + + /** + * @CORS + */ + #[PublicPage] + public function testCORSShouldNeverAllowCookieAuthAttribute() { + } + + /** + * @PublicPage + */ + #[CORS] + public function testCORSAttributeShouldNeverAllowCookieAuth() { + } + + #[CORS] + #[PublicPage] + public function testCORSAttributeShouldNeverAllowCookieAuthAttribute() { + } + + /** + * @CORS + */ + public function testCORSShouldRelogin() { + } + + #[CORS] + public function testCORSAttributeShouldRelogin() { + } + + /** + * @CORS + */ + public function testCORSShouldFailIfPasswordLoginIsForbidden() { + } + + #[CORS] + public function testCORSAttributeShouldFailIfPasswordLoginIsForbidden() { + } + + /** + * @CORS + */ + public function testCORSShouldNotAllowCookieAuth() { + } + + #[CORS] + public function testCORSAttributeShouldNotAllowCookieAuth() { + } + + public function testAfterExceptionWithSecurityExceptionNoStatus() { + } + + public function testAfterExceptionWithSecurityExceptionWithStatus() { + } + + + public function testAfterExceptionWithRegularException() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php b/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php new file mode 100644 index 00000000000..4d6778e98b9 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; + +class NormalController extends Controller { + public function foo() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php b/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php new file mode 100644 index 00000000000..93e793ecca9 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +class OCSController extends \OCP\AppFramework\OCSController { + public function foo() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php new file mode 100644 index 00000000000..cd1cdaa49ca --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; + +class PasswordConfirmationMiddlewareController extends Controller { + public function testNoAnnotationNorAttribute() { + } + + /** + * @TestAnnotation + */ + public function testDifferentAnnotation() { + } + + /** + * @PasswordConfirmationRequired + */ + public function testAnnotation() { + } + + #[PasswordConfirmationRequired] + public function testAttribute() { + } + + #[PasswordConfirmationRequired] + public function testSSO() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php new file mode 100644 index 00000000000..c8f9878b0c1 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php @@ -0,0 +1,171 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security\Mock; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\ExAppRequired; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\StrictCookiesRequired; +use OCP\AppFramework\Http\Attribute\SubAdminRequired; + +class SecurityMiddlewareController extends Controller { + /** + * @PublicPage + * @NoCSRFRequired + */ + public function testAnnotationNoCSRFRequiredPublicPage() { + } + + /** + * @NoCSRFRequired + */ + #[PublicPage] + public function testAnnotationNoCSRFRequiredAttributePublicPage() { + } + + /** + * @PublicPage + */ + #[NoCSRFRequired] + public function testAnnotationPublicPageAttributeNoCSRFRequired() { + } + + #[NoCSRFRequired] + #[PublicPage] + public function testAttributeNoCSRFRequiredPublicPage() { + } + + public function testNoAnnotationNorAttribute() { + } + + /** + * @NoCSRFRequired + */ + public function testAnnotationNoCSRFRequired() { + } + + #[NoCSRFRequired] + public function testAttributeNoCSRFRequired() { + } + + /** + * @PublicPage + */ + public function testAnnotationPublicPage() { + } + + #[PublicPage] + public function testAttributePublicPage() { + } + + /** + * @PublicPage + * @StrictCookieRequired + */ + public function testAnnotationPublicPageStrictCookieRequired() { + } + + /** + * @StrictCookieRequired + */ + #[PublicPage] + public function testAnnotationStrictCookieRequiredAttributePublicPage() { + } + + /** + * @PublicPage + */ + #[StrictCookiesRequired] + public function testAnnotationPublicPageAttributeStrictCookiesRequired() { + } + + #[PublicPage] + #[StrictCookiesRequired] + public function testAttributePublicPageStrictCookiesRequired() { + } + + /** + * @PublicPage + * @NoCSRFRequired + * @StrictCookieRequired + */ + public function testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired() { + } + + #[NoCSRFRequired] + #[PublicPage] + #[StrictCookiesRequired] + public function testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired() { + } + + /** + * @NoCSRFRequired + * @NoAdminRequired + */ + public function testAnnotationNoAdminRequiredNoCSRFRequired() { + } + + #[NoAdminRequired] + #[NoCSRFRequired] + public function testAttributeNoAdminRequiredNoCSRFRequired() { + } + + /** + * @NoCSRFRequired + * @SubAdminRequired + */ + public function testAnnotationNoCSRFRequiredSubAdminRequired() { + } + + /** + * @SubAdminRequired + */ + #[NoCSRFRequired] + public function testAnnotationNoCSRFRequiredAttributeSubAdminRequired() { + } + + /** + * @NoCSRFRequired + */ + #[SubAdminRequired] + public function testAnnotationSubAdminRequiredAttributeNoCSRFRequired() { + } + + #[NoCSRFRequired] + #[SubAdminRequired] + public function testAttributeNoCSRFRequiredSubAdminRequired() { + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage() { + } + + #[NoAdminRequired] + #[NoCSRFRequired] + #[PublicPage] + public function testAttributeNoAdminRequiredNoCSRFRequiredPublicPage() { + } + + /** + * @ExAppRequired + */ + public function testAnnotationExAppRequired() { + } + + #[ExAppRequired] + public function testAttributeExAppRequired() { + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php new file mode 100644 index 00000000000..90e801ca471 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php @@ -0,0 +1,209 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; +use OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Authentication\Token\IProvider; +use OC\User\Manager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\Token\IToken; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Test\AppFramework\Middleware\Security\Mock\PasswordConfirmationMiddlewareController; +use Test\TestCase; + +class PasswordConfirmationMiddlewareTest extends TestCase { + /** @var ControllerMethodReflector */ + private $reflector; + /** @var ISession&\PHPUnit\Framework\MockObject\MockObject */ + private $session; + /** @var IUserSession&\PHPUnit\Framework\MockObject\MockObject */ + private $userSession; + /** @var IUser&\PHPUnit\Framework\MockObject\MockObject */ + private $user; + /** @var PasswordConfirmationMiddleware */ + private $middleware; + /** @var PasswordConfirmationMiddlewareController */ + private $controller; + /** @var ITimeFactory&\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + private IProvider&\PHPUnit\Framework\MockObject\MockObject $tokenProvider; + private LoggerInterface $logger; + /** @var IRequest&\PHPUnit\Framework\MockObject\MockObject */ + private IRequest $request; + /** @var Manager&\PHPUnit\Framework\MockObject\MockObject */ + private Manager $userManager; + + protected function setUp(): void { + $this->reflector = new ControllerMethodReflector(); + $this->session = $this->createMock(ISession::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->user = $this->createMock(IUser::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->tokenProvider = $this->createMock(IProvider::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->request = $this->createMock(IRequest::class); + $this->userManager = $this->createMock(Manager::class); + $this->controller = new PasswordConfirmationMiddlewareController( + 'test', + $this->createMock(IRequest::class) + ); + + $this->middleware = new PasswordConfirmationMiddleware( + $this->reflector, + $this->session, + $this->userSession, + $this->timeFactory, + $this->tokenProvider, + $this->logger, + $this->request, + $this->userManager, + ); + } + + public function testNoAnnotationNorAttribute(): void { + $this->reflector->reflect($this->controller, __FUNCTION__); + $this->session->expects($this->never()) + ->method($this->anything()); + $this->userSession->expects($this->never()) + ->method($this->anything()); + + $this->middleware->beforeController($this->controller, __FUNCTION__); + } + + public function testDifferentAnnotation(): void { + $this->reflector->reflect($this->controller, __FUNCTION__); + $this->session->expects($this->never()) + ->method($this->anything()); + $this->userSession->expects($this->never()) + ->method($this->anything()); + + $this->middleware->beforeController($this->controller, __FUNCTION__); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] + public function testAnnotation($backend, $lastConfirm, $currentTime, $exception): void { + $this->reflector->reflect($this->controller, __FUNCTION__); + + $this->user->method('getBackendClassName') + ->willReturn($backend); + $this->userSession->method('getUser') + ->willReturn($this->user); + + $this->session->method('get') + ->with('last-password-confirm') + ->willReturn($lastConfirm); + + $this->timeFactory->method('getTime') + ->willReturn($currentTime); + + $token = $this->createMock(IToken::class); + $token->method('getScopeAsArray') + ->willReturn([]); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->willReturn($token); + + $thrown = false; + try { + $this->middleware->beforeController($this->controller, __FUNCTION__); + } catch (NotConfirmedException $e) { + $thrown = true; + } + + $this->assertSame($exception, $thrown); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] + public function testAttribute($backend, $lastConfirm, $currentTime, $exception): void { + $this->reflector->reflect($this->controller, __FUNCTION__); + + $this->user->method('getBackendClassName') + ->willReturn($backend); + $this->userSession->method('getUser') + ->willReturn($this->user); + + $this->session->method('get') + ->with('last-password-confirm') + ->willReturn($lastConfirm); + + $this->timeFactory->method('getTime') + ->willReturn($currentTime); + + $token = $this->createMock(IToken::class); + $token->method('getScopeAsArray') + ->willReturn([]); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->willReturn($token); + + $thrown = false; + try { + $this->middleware->beforeController($this->controller, __FUNCTION__); + } catch (NotConfirmedException $e) { + $thrown = true; + } + + $this->assertSame($exception, $thrown); + } + + + + public static function dataProvider(): array { + return [ + ['foo', 2000, 4000, true], + ['foo', 2000, 3000, false], + ['user_saml', 2000, 4000, false], + ['user_saml', 2000, 3000, false], + ['foo', 2000, 3815, false], + ['foo', 2000, 3816, true], + ]; + } + + public function testSSO(): void { + static $sessionId = 'mySession1d'; + + $this->reflector->reflect($this->controller, __FUNCTION__); + + $this->user->method('getBackendClassName') + ->willReturn('fictional_backend'); + $this->userSession->method('getUser') + ->willReturn($this->user); + + $this->session->method('get') + ->with('last-password-confirm') + ->willReturn(0); + $this->session->method('getId') + ->willReturn($sessionId); + + $this->timeFactory->method('getTime') + ->willReturn(9876); + + $token = $this->createMock(IToken::class); + $token->method('getScopeAsArray') + ->willReturn([IToken::SCOPE_SKIP_PASSWORD_VALIDATION => true]); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with($sessionId) + ->willReturn($token); + + $thrown = false; + try { + $this->middleware->beforeController($this->controller, __FUNCTION__); + } catch (NotConfirmedException) { + $thrown = true; + } + + $this->assertSame(false, $thrown); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php new file mode 100644 index 00000000000..c42baadcb1c --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php @@ -0,0 +1,322 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\RateLimitingMiddleware; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\Ip\BruteforceAllowList; +use OC\Security\RateLimiting\Exception\RateLimitExceededException; +use OC\Security\RateLimiting\Limiter; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; +use OCP\AppFramework\Http\Attribute\UserRateLimit; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IAppConfig; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class TestRateLimitController extends Controller { + /** + * @UserRateThrottle(limit=20, period=200) + * @AnonRateThrottle(limit=10, period=100) + */ + public function testMethodWithAnnotation() { + } + + /** + * @AnonRateThrottle(limit=10, period=100) + */ + public function testMethodWithAnnotationFallback() { + } + + public function testMethodWithoutAnnotation() { + } + + #[UserRateLimit(limit: 20, period: 200)] + #[AnonRateLimit(limit: 10, period: 100)] + public function testMethodWithAttributes() { + } + + #[AnonRateLimit(limit: 10, period: 100)] + public function testMethodWithAttributesFallback() { + } +} + +/** + * @group DB + */ +class RateLimitingMiddlewareTest extends TestCase { + private IRequest|MockObject $request; + private IUserSession|MockObject $userSession; + private ControllerMethodReflector $reflector; + private Limiter|MockObject $limiter; + private ISession|MockObject $session; + private IAppConfig|MockObject $appConfig; + private BruteforceAllowList|MockObject $bruteForceAllowList; + private RateLimitingMiddleware $rateLimitingMiddleware; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->reflector = new ControllerMethodReflector(); + $this->limiter = $this->createMock(Limiter::class); + $this->session = $this->createMock(ISession::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->bruteForceAllowList = $this->createMock(BruteforceAllowList::class); + + $this->rateLimitingMiddleware = new RateLimitingMiddleware( + $this->request, + $this->userSession, + $this->reflector, + $this->limiter, + $this->session, + $this->appConfig, + $this->bruteForceAllowList, + ); + } + + public function testBeforeControllerWithoutAnnotationForAnon(): void { + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + /** @var TestRateLimitController|MockObject $controller */ + $controller = $this->createMock(TestRateLimitController::class); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); + } + + public function testBeforeControllerWithoutAnnotationForLoggedIn(): void { + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + + /** @var TestRateLimitController|MockObject $controller */ + $controller = $this->createMock(TestRateLimitController::class); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); + } + + public function testBeforeControllerForAnon(): void { + $controller = new TestRateLimitController('test', $this->request); + + $this->request + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAnnotation', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); + } + + public function testBeforeControllerForLoggedIn(): void { + $controller = new TestRateLimitController('test', $this->request); + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerUserRequest') + ->with(get_class($controller) . '::testMethodWithAnnotation', '20', '200', $user); + + + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); + } + + public function testBeforeControllerAnonWithFallback(): void { + $controller = new TestRateLimitController('test', $this->request); + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + + + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAnnotationFallback', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAnnotationFallback'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotationFallback'); + } + + public function testBeforeControllerAttributesForAnon(): void { + $controller = new TestRateLimitController('test', $this->request); + + $this->request + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAttributes', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAttributes'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); + } + + public function testBeforeControllerAttributesForLoggedIn(): void { + $controller = new TestRateLimitController('test', $this->request); + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerUserRequest') + ->with(get_class($controller) . '::testMethodWithAttributes', '20', '200', $user); + + + $this->reflector->reflect($controller, 'testMethodWithAttributes'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); + } + + public function testBeforeControllerAttributesAnonWithFallback(): void { + $controller = new TestRateLimitController('test', $this->request); + $this->request + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + + + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAttributesFallback', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAttributesFallback'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributesFallback'); + } + + public function testAfterExceptionWithOtherException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('My test exception'); + + $controller = new TestRateLimitController('test', $this->request); + + $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new \Exception('My test exception')); + } + + public function testAfterExceptionWithJsonBody(): void { + $controller = new TestRateLimitController('test', $this->request); + $this->request + ->expects($this->once()) + ->method('getHeader') + ->with('Accept') + ->willReturn('JSON'); + + $result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException()); + $expected = new DataResponse([], 429 + ); + $this->assertEquals($expected, $result); + } + + public function testAfterExceptionWithHtmlBody(): void { + $controller = new TestRateLimitController('test', $this->request); + $this->request + ->expects($this->once()) + ->method('getHeader') + ->with('Accept') + ->willReturn('html'); + + $result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException()); + $expected = new TemplateResponse( + 'core', + '429', + [], + TemplateResponse::RENDER_AS_GUEST + ); + $expected->setStatus(429); + $this->assertEquals($expected, $result); + $this->assertIsString($result->render()); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php new file mode 100644 index 00000000000..7800371f68f --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php @@ -0,0 +1,120 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Http\Request; +use OC\AppFramework\Middleware\Security\Exceptions\LaxSameSiteCookieFailedException; +use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; +use OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use Test\TestCase; + +class SameSiteCookieMiddlewareTest extends TestCase { + /** @var SameSiteCookieMiddleware */ + private $middleware; + + /** @var Request|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + + /** @var ControllerMethodReflector|\PHPUnit\Framework\MockObject\MockObject */ + private $reflector; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(Request::class); + $this->reflector = $this->createMock(ControllerMethodReflector::class); + $this->middleware = new SameSiteCookieMiddleware($this->request, $this->reflector); + } + + public function testBeforeControllerNoIndex(): void { + $this->request->method('getScriptName') + ->willReturn('/ocs/v2.php'); + + $this->middleware->beforeController($this->createMock(Controller::class), 'foo'); + $this->addToAssertionCount(1); + } + + public function testBeforeControllerIndexHasAnnotation(): void { + $this->request->method('getScriptName') + ->willReturn('/index.php'); + + $this->reflector->method('hasAnnotation') + ->with('NoSameSiteCookieRequired') + ->willReturn(true); + + $this->middleware->beforeController($this->createMock(Controller::class), 'foo'); + $this->addToAssertionCount(1); + } + + public function testBeforeControllerIndexNoAnnotationPassingCheck(): void { + $this->request->method('getScriptName') + ->willReturn('/index.php'); + + $this->reflector->method('hasAnnotation') + ->with('NoSameSiteCookieRequired') + ->willReturn(false); + + $this->request->method('passesLaxCookieCheck') + ->willReturn(true); + + $this->middleware->beforeController($this->createMock(Controller::class), 'foo'); + $this->addToAssertionCount(1); + } + + public function testBeforeControllerIndexNoAnnotationFailingCheck(): void { + $this->expectException(LaxSameSiteCookieFailedException::class); + + $this->request->method('getScriptName') + ->willReturn('/index.php'); + + $this->reflector->method('hasAnnotation') + ->with('NoSameSiteCookieRequired') + ->willReturn(false); + + $this->request->method('passesLaxCookieCheck') + ->willReturn(false); + + $this->middleware->beforeController($this->createMock(Controller::class), 'foo'); + } + + public function testAfterExceptionNoLaxCookie(): void { + $ex = new SecurityException(); + + try { + $this->middleware->afterException($this->createMock(Controller::class), 'foo', $ex); + $this->fail(); + } catch (\Exception $e) { + $this->assertSame($ex, $e); + } + } + + public function testAfterExceptionLaxCookie(): void { + $ex = new LaxSameSiteCookieFailedException(); + + $this->request->method('getRequestUri') + ->willReturn('/myrequri'); + + $middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class) + ->setConstructorArgs([$this->request, $this->reflector]) + ->onlyMethods(['setSameSiteCookie']) + ->getMock(); + + $middleware->expects($this->once()) + ->method('setSameSiteCookie'); + + $resp = $middleware->afterException($this->createMock(Controller::class), 'foo', $ex); + + $this->assertSame(Http::STATUS_FOUND, $resp->getStatus()); + + $headers = $resp->getHeaders(); + $this->assertSame('/myrequri', $headers['Location']); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php new file mode 100644 index 00000000000..0c6fc21357d --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php @@ -0,0 +1,701 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\AppFramework\Middleware\Security; + +use OC\AppFramework\Http; +use OC\AppFramework\Http\Request; +use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException; +use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException; +use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; +use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; +use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException; +use OC\AppFramework\Middleware\Security\SecurityMiddleware; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Settings\AuthorizedGroupMapper; +use OC\User\Session; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Group\ISubAdmin; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\INavigationManager; +use OCP\IRequest; +use OCP\IRequestId; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; +use Psr\Log\LoggerInterface; +use Test\AppFramework\Middleware\Security\Mock\NormalController; +use Test\AppFramework\Middleware\Security\Mock\OCSController; +use Test\AppFramework\Middleware\Security\Mock\SecurityMiddlewareController; + +class SecurityMiddlewareTest extends \Test\TestCase { + /** @var SecurityMiddleware|\PHPUnit\Framework\MockObject\MockObject */ + private $middleware; + /** @var SecurityMiddlewareController */ + private $controller; + /** @var SecurityException */ + private $secException; + /** @var SecurityException */ + private $secAjaxException; + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var ControllerMethodReflector */ + private $reader; + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + /** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */ + private $navigationManager; + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + private $urlGenerator; + /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */ + private $appManager; + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l10n; + /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ + private $userSession; + /** @var AuthorizedGroupMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $authorizedGroupMapper; + + protected function setUp(): void { + parent::setUp(); + + $this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class); + $this->userSession = $this->createMock(Session::class); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('test'); + $this->userSession->method('getUser')->willReturn($user); + $this->request = $this->createMock(IRequest::class); + $this->controller = new SecurityMiddlewareController( + 'test', + $this->request + ); + $this->reader = new ControllerMethodReflector(); + $this->logger = $this->createMock(LoggerInterface::class); + $this->navigationManager = $this->createMock(INavigationManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->l10n = $this->createMock(IL10N::class); + $this->middleware = $this->getMiddleware(true, true, false); + $this->secException = new SecurityException('hey', false); + $this->secAjaxException = new SecurityException('hey', true); + } + + private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware { + $this->appManager = $this->createMock(IAppManager::class); + $this->appManager->expects($this->any()) + ->method('isEnabledForUser') + ->willReturn($isAppEnabledForUser); + $remoteIpAddress = $this->createMock(IRemoteAddress::class); + $remoteIpAddress->method('allowsAdminActions')->willReturn(true); + + $groupManager = $this->createMock(IGroupManager::class); + $groupManager->method('isAdmin') + ->willReturn($isAdminUser); + $subAdminManager = $this->createMock(ISubAdmin::class); + $subAdminManager->method('isSubAdmin') + ->willReturn($isSubAdmin); + + return new SecurityMiddleware( + $this->request, + $this->reader, + $this->navigationManager, + $this->urlGenerator, + $this->logger, + 'files', + $isLoggedIn, + $groupManager, + $subAdminManager, + $this->appManager, + $this->l10n, + $this->authorizedGroupMapper, + $this->userSession, + $remoteIpAddress + ); + } + + public static function dataNoCSRFRequiredPublicPage(): array { + return [ + ['testAnnotationNoCSRFRequiredPublicPage'], + ['testAnnotationNoCSRFRequiredAttributePublicPage'], + ['testAnnotationPublicPageAttributeNoCSRFRequired'], + ['testAttributeNoCSRFRequiredPublicPage'], + ]; + } + + public static function dataPublicPage(): array { + return [ + ['testAnnotationPublicPage'], + ['testAttributePublicPage'], + ]; + } + + public static function dataNoCSRFRequired(): array { + return [ + ['testAnnotationNoCSRFRequired'], + ['testAttributeNoCSRFRequired'], + ]; + } + + public static function dataPublicPageStrictCookieRequired(): array { + return [ + ['testAnnotationPublicPageStrictCookieRequired'], + ['testAnnotationStrictCookieRequiredAttributePublicPage'], + ['testAnnotationPublicPageAttributeStrictCookiesRequired'], + ['testAttributePublicPageStrictCookiesRequired'], + ]; + } + + public static function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array { + return [ + ['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'], + ['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'], + ]; + } + + public static function dataNoAdminRequiredNoCSRFRequired(): array { + return [ + ['testAnnotationNoAdminRequiredNoCSRFRequired'], + ['testAttributeNoAdminRequiredNoCSRFRequired'], + ]; + } + + public static function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array { + return [ + ['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'], + ['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'], + ]; + } + + public static function dataNoCSRFRequiredSubAdminRequired(): array { + return [ + ['testAnnotationNoCSRFRequiredSubAdminRequired'], + ['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'], + ['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'], + ['testAttributeNoCSRFRequiredSubAdminRequired'], + ]; + } + + public static function dataExAppRequired(): array { + return [ + ['testAnnotationExAppRequired'], + ['testAttributeExAppRequired'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + public function testSetNavigationEntry(string $method): void { + $this->navigationManager->expects($this->once()) + ->method('setActiveEntry') + ->with($this->equalTo('files')); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + + /** + * @param string $method + * @param string $test + */ + private function ajaxExceptionStatus($method, $test, $status) { + $isLoggedIn = false; + $isAdminUser = false; + + // isAdminUser requires isLoggedIn call to return true + if ($test === 'isAdminUser') { + $isLoggedIn = true; + } + + $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false); + + try { + $this->reader->reflect($this->controller, $method); + $sec->beforeController($this->controller, $method); + } catch (SecurityException $ex) { + $this->assertEquals($status, $ex->getCode()); + } + + // add assertion if everything should work fine otherwise phpunit will + // complain + if ($status === 0) { + $this->addToAssertionCount(1); + } + } + + public function testAjaxStatusLoggedInCheck(): void { + $this->ajaxExceptionStatus( + 'testNoAnnotationNorAttribute', + 'isLoggedIn', + Http::STATUS_UNAUTHORIZED + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] + public function testAjaxNotAdminCheck(string $method): void { + $this->ajaxExceptionStatus( + $method, + 'isAdminUser', + Http::STATUS_FORBIDDEN + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testAjaxStatusCSRFCheck(string $method): void { + $this->ajaxExceptionStatus( + $method, + 'passesCSRFCheck', + Http::STATUS_PRECONDITION_FAILED + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + public function testAjaxStatusAllGood(string $method): void { + $this->ajaxExceptionStatus( + $method, + 'isLoggedIn', + 0 + ); + $this->ajaxExceptionStatus( + $method, + 'isAdminUser', + 0 + ); + $this->ajaxExceptionStatus( + $method, + 'passesCSRFCheck', + 0 + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + public function testNoChecks(string $method): void { + $this->request->expects($this->never()) + ->method('passesCSRFCheck') + ->willReturn(false); + + $sec = $this->getMiddleware(false, false, false); + + $this->reader->reflect($this->controller, $method); + $sec->beforeController($this->controller, $method); + } + + /** + * @param string $method + * @param string $expects + */ + private function securityCheck($method, $expects, $shouldFail = false) { + // admin check requires login + if ($expects === 'isAdminUser') { + $isLoggedIn = true; + $isAdminUser = !$shouldFail; + } else { + $isLoggedIn = !$shouldFail; + $isAdminUser = false; + } + + $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false); + + if ($shouldFail) { + $this->expectException(SecurityException::class); + } else { + $this->addToAssertionCount(1); + } + + $this->reader->reflect($this->controller, $method); + $sec->beforeController($this->controller, $method); + } + + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testCsrfCheck(string $method): void { + $this->expectException(CrossSiteRequestForgeryException::class); + + $this->request->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + public function testNoCsrfCheck(string $method): void { + $this->request->expects($this->never()) + ->method('passesCSRFCheck') + ->willReturn(false); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testPassesCsrfCheck(string $method): void { + $this->request->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] + public function testFailCsrfCheck(string $method): void { + $this->expectException(CrossSiteRequestForgeryException::class); + + $this->request->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPageStrictCookieRequired')] + public function testStrictCookieRequiredCheck(string $method): void { + $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class); + + $this->request->expects($this->never()) + ->method('passesCSRFCheck'); + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(false); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] + public function testNoStrictCookieRequiredCheck(string $method): void { + $this->request->expects($this->never()) + ->method('passesStrictCookieCheck') + ->willReturn(false); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPageStrictCookieRequired')] + public function testPassesStrictCookieRequiredCheck(string $method): void { + $this->request + ->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + + $this->reader->reflect($this->controller, $method); + $this->middleware->beforeController($this->controller, $method); + } + + public static function dataCsrfOcsController(): array { + return [ + [NormalController::class, false, false, true], + [NormalController::class, false, true, true], + [NormalController::class, true, false, true], + [NormalController::class, true, true, true], + + [OCSController::class, false, false, true], + [OCSController::class, false, true, false], + [OCSController::class, true, false, false], + [OCSController::class, true, true, false], + ]; + } + + /** + * @param string $controllerClass + * @param bool $hasOcsApiHeader + * @param bool $hasBearerAuth + * @param bool $exception + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataCsrfOcsController')] + public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void { + $this->request + ->method('getHeader') + ->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) { + if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) { + return 'true'; + } + if ($header === 'Authorization' && $hasBearerAuth) { + return 'Bearer TOKEN!'; + } + return ''; + }); + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + + $controller = new $controllerClass('test', $this->request); + + try { + $this->middleware->beforeController($controller, 'foo'); + $this->assertFalse($exception); + } catch (CrossSiteRequestForgeryException $e) { + $this->assertTrue($exception); + } + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] + public function testLoggedInCheck(string $method): void { + $this->securityCheck($method, 'isLoggedIn'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] + public function testFailLoggedInCheck(string $method): void { + $this->securityCheck($method, 'isLoggedIn', true); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] + public function testIsAdminCheck(string $method): void { + $this->securityCheck($method, 'isAdminUser'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] + public function testIsNotSubAdminCheck(string $method): void { + $this->reader->reflect($this->controller, $method); + $sec = $this->getMiddleware(true, false, false); + + $this->expectException(SecurityException::class); + $sec->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] + public function testIsSubAdminCheck(string $method): void { + $this->reader->reflect($this->controller, $method); + $sec = $this->getMiddleware(true, false, true); + + $sec->beforeController($this->controller, $method); + $this->addToAssertionCount(1); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] + public function testIsSubAdminAndAdminCheck(string $method): void { + $this->reader->reflect($this->controller, $method); + $sec = $this->getMiddleware(true, true, true); + + $sec->beforeController($this->controller, $method); + $this->addToAssertionCount(1); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] + public function testFailIsAdminCheck(string $method): void { + $this->securityCheck($method, 'isAdminUser', true); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')] + public function testRestrictedAppLoggedInPublicPage(string $method): void { + $middleware = $this->getMiddleware(true, false, false); + $this->reader->reflect($this->controller, $method); + + $this->appManager->method('getAppPath') + ->with('files') + ->willReturn('foo'); + + $this->appManager->method('isEnabledForUser') + ->with('files') + ->willReturn(false); + + $middleware->beforeController($this->controller, $method); + $this->addToAssertionCount(1); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')] + public function testRestrictedAppNotLoggedInPublicPage(string $method): void { + $middleware = $this->getMiddleware(false, false, false); + $this->reader->reflect($this->controller, $method); + + $this->appManager->method('getAppPath') + ->with('files') + ->willReturn('foo'); + + $this->appManager->method('isEnabledForUser') + ->with('files') + ->willReturn(false); + + $middleware->beforeController($this->controller, $method); + $this->addToAssertionCount(1); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] + public function testRestrictedAppLoggedIn(string $method): void { + $middleware = $this->getMiddleware(true, false, false, false); + $this->reader->reflect($this->controller, $method); + + $this->appManager->method('getAppPath') + ->with('files') + ->willReturn('foo'); + + $this->expectException(AppNotEnabledException::class); + $middleware->beforeController($this->controller, $method); + } + + + public function testAfterExceptionNotCaughtThrowsItAgain(): void { + $ex = new \Exception(); + $this->expectException(\Exception::class); + $this->middleware->afterException($this->controller, 'test', $ex); + } + + public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void { + $this->request = new Request( + [ + 'server' + => [ + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp' + ] + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->middleware = $this->getMiddleware(false, false, false); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.login.showLoginForm', + [ + 'redirect_url' => 'nextcloud/index.php/apps/specialapp', + ] + ) + ->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp'); + $this->logger + ->expects($this->once()) + ->method('debug'); + $response = $this->middleware->afterException( + $this->controller, + 'test', + new NotLoggedInException() + ); + $expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp'); + $this->assertEquals($expected, $response); + } + + public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void { + $this->request = new Request( + [ + 'server' => [ + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp', + ], + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + + $this->middleware = $this->getMiddleware(false, false, false); + $response = $this->middleware->afterException( + $this->controller, + 'test', + new StrictCookieMissingException() + ); + + $expected = new RedirectResponse(\OC::$WEBROOT . '/'); + $this->assertEquals($expected, $response); + } + + + /** + * @return array + */ + public static function exceptionProvider(): array { + return [ + [ + new AppNotEnabledException(), + ], + [ + new CrossSiteRequestForgeryException(), + ], + [ + new NotAdminException(''), + ], + ]; + } + + /** + * @param SecurityException $exception + */ + #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')] + public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void { + $this->request = new Request( + [ + 'server' + => [ + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp' + ] + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->middleware = $this->getMiddleware(false, false, false); + $this->logger + ->expects($this->once()) + ->method('debug'); + $response = $this->middleware->afterException( + $this->controller, + 'test', + $exception + ); + $expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest'); + $expected->setStatus($exception->getCode()); + $this->assertEquals($expected, $response); + } + + public function testAfterAjaxExceptionReturnsJSONError(): void { + $response = $this->middleware->afterException($this->controller, 'test', + $this->secAjaxException); + + $this->assertTrue($response instanceof JSONResponse); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')] + public function testExAppRequired(string $method): void { + $middleware = $this->getMiddleware(true, false, false); + $this->reader->reflect($this->controller, $method); + + $session = $this->createMock(ISession::class); + $session->method('get')->with('app_api')->willReturn(true); + $this->userSession->method('getSession')->willReturn($session); + + $this->request->expects($this->once()) + ->method('passesStrictCookieCheck') + ->willReturn(true); + $this->request->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); + + $middleware->beforeController($this->controller, $method); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')] + public function testExAppRequiredError(string $method): void { + $middleware = $this->getMiddleware(true, false, false, false); + $this->reader->reflect($this->controller, $method); + + $session = $this->createMock(ISession::class); + $session->method('get')->with('app_api')->willReturn(false); + $this->userSession->method('getSession')->willReturn($session); + + $this->expectException(ExAppRequiredException::class); + $middleware->beforeController($this->controller, $method); + } +} diff --git a/tests/lib/AppFramework/Middleware/SessionMiddlewareTest.php b/tests/lib/AppFramework/Middleware/SessionMiddlewareTest.php new file mode 100644 index 00000000000..8ecc73791c9 --- /dev/null +++ b/tests/lib/AppFramework/Middleware/SessionMiddlewareTest.php @@ -0,0 +1,138 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\AppFramework\Middleware; + +use OC\AppFramework\Middleware\SessionMiddleware; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\UseSession; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; +use OCP\ISession; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class SessionMiddlewareTest extends TestCase { + private ControllerMethodReflector|MockObject $reflector; + private ISession|MockObject $session; + private Controller $controller; + private SessionMiddleware $middleware; + + protected function setUp(): void { + parent::setUp(); + + $this->reflector = $this->createMock(ControllerMethodReflector::class); + $this->session = $this->createMock(ISession::class); + $this->controller = new class('app', $this->createMock(IRequest::class)) extends Controller { + /** + * @UseSession + */ + public function withAnnotation() { + } + #[UseSession] + public function withAttribute() { + } + public function without() { + } + }; + $this->middleware = new SessionMiddleware( + $this->reflector, + $this->session, + ); + } + + public function testSessionNotClosedOnBeforeController(): void { + $this->configureSessionMock(0, 1); + $this->reflector->expects(self::once()) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(true); + + $this->middleware->beforeController($this->controller, 'withAnnotation'); + } + + public function testSessionNotClosedOnBeforeControllerWithAttribute(): void { + $this->configureSessionMock(0, 1); + $this->reflector->expects(self::once()) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(false); + + $this->middleware->beforeController($this->controller, 'withAttribute'); + } + + public function testSessionClosedOnAfterController(): void { + $this->configureSessionMock(1); + $this->reflector->expects(self::once()) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(true); + + $this->middleware->afterController($this->controller, 'withAnnotation', new Response()); + } + + public function testSessionClosedOnAfterControllerWithAttribute(): void { + $this->configureSessionMock(1); + $this->reflector->expects(self::once()) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(true); + + $this->middleware->afterController($this->controller, 'withAttribute', new Response()); + } + + public function testSessionReopenedAndClosedOnBeforeController(): void { + $this->configureSessionMock(1, 1); + $this->reflector->expects(self::exactly(2)) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(true); + + $this->middleware->beforeController($this->controller, 'withAnnotation'); + $this->middleware->afterController($this->controller, 'withAnnotation', new Response()); + } + + public function testSessionReopenedAndClosedOnBeforeControllerWithAttribute(): void { + $this->configureSessionMock(1, 1); + $this->reflector->expects(self::exactly(2)) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(false); + + $this->middleware->beforeController($this->controller, 'withAttribute'); + $this->middleware->afterController($this->controller, 'withAttribute', new Response()); + } + + public function testSessionClosedOnBeforeController(): void { + $this->configureSessionMock(0); + $this->reflector->expects(self::once()) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(false); + + $this->middleware->beforeController($this->controller, 'without'); + } + + public function testSessionNotClosedOnAfterController(): void { + $this->configureSessionMock(0); + $this->reflector->expects(self::once()) + ->method('hasAnnotation') + ->with('UseSession') + ->willReturn(false); + + $this->middleware->afterController($this->controller, 'without', new Response()); + } + + private function configureSessionMock(int $expectedCloseCount, int $expectedReopenCount = 0): void { + $this->session->expects($this->exactly($expectedCloseCount)) + ->method('close'); + $this->session->expects($this->exactly($expectedReopenCount)) + ->method('reopen'); + } +} diff --git a/tests/lib/AppFramework/OCS/BaseResponseTest.php b/tests/lib/AppFramework/OCS/BaseResponseTest.php new file mode 100644 index 00000000000..e04f7856623 --- /dev/null +++ b/tests/lib/AppFramework/OCS/BaseResponseTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\OCS; + +use OC\AppFramework\OCS\BaseResponse; + +class ArrayValue implements \JsonSerializable { + public function __construct( + private array $array, + ) { + } + + public function jsonSerialize(): mixed { + return $this->array; + } +} + +class BaseResponseTest extends \Test\TestCase { + public function testToXml(): void { + /** @var BaseResponse $response */ + $response = $this->createMock(BaseResponse::class); + + $writer = new \XMLWriter(); + $writer->openMemory(); + $writer->setIndent(false); + $writer->startDocument(); + + $data = [ + 'hello' => 'hello', + 'information' => [ + '@test' => 'some data', + 'someElement' => 'withAttribute', + ], + 'value without key', + 'object' => new \stdClass(), + ]; + + $this->invokePrivate($response, 'toXml', [$data, $writer]); + $writer->endDocument(); + + $this->assertEquals( + "<?xml version=\"1.0\"?>\n<hello>hello</hello><information test=\"some data\"><someElement>withAttribute</someElement></information><element>value without key</element><object/>\n", + $writer->outputMemory(true) + ); + } + + public function testToXmlJsonSerializable(): void { + /** @var BaseResponse $response */ + $response = $this->createMock(BaseResponse::class); + + $writer = new \XMLWriter(); + $writer->openMemory(); + $writer->setIndent(false); + $writer->startDocument(); + + $data = [ + 'hello' => 'hello', + 'information' => new ArrayValue([ + '@test' => 'some data', + 'someElement' => 'withAttribute', + ]), + 'value without key', + 'object' => new \stdClass(), + ]; + + $this->invokePrivate($response, 'toXml', [$data, $writer]); + $writer->endDocument(); + + $this->assertEquals( + "<?xml version=\"1.0\"?>\n<hello>hello</hello><information test=\"some data\"><someElement>withAttribute</someElement></information><element>value without key</element><object/>\n", + $writer->outputMemory(true) + ); + } +} diff --git a/tests/lib/AppFramework/OCS/V2ResponseTest.php b/tests/lib/AppFramework/OCS/V2ResponseTest.php new file mode 100644 index 00000000000..7a70ad6d633 --- /dev/null +++ b/tests/lib/AppFramework/OCS/V2ResponseTest.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\OCS; + +use OC\AppFramework\OCS\V2Response; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; + +class V2ResponseTest extends \Test\TestCase { + #[\PHPUnit\Framework\Attributes\DataProvider('providesStatusCodes')] + public function testStatusCodeMapper(int $expected, int $sc): void { + $response = new V2Response(new DataResponse([], $sc)); + $this->assertEquals($expected, $response->getStatus()); + } + + public static function providesStatusCodes(): array { + return [ + [Http::STATUS_OK, 200], + [Http::STATUS_BAD_REQUEST, 104], + [Http::STATUS_BAD_REQUEST, 1000], + [201, 201], + [Http::STATUS_UNAUTHORIZED, OCSController::RESPOND_UNAUTHORISED], + [Http::STATUS_INTERNAL_SERVER_ERROR, OCSController::RESPOND_SERVER_ERROR], + [Http::STATUS_NOT_FOUND, OCSController::RESPOND_NOT_FOUND], + [Http::STATUS_INTERNAL_SERVER_ERROR, OCSController::RESPOND_UNKNOWN_ERROR], + ]; + } +} diff --git a/tests/lib/AppFramework/Routing/RouteParserTest.php b/tests/lib/AppFramework/Routing/RouteParserTest.php new file mode 100644 index 00000000000..406c5f1f3a5 --- /dev/null +++ b/tests/lib/AppFramework/Routing/RouteParserTest.php @@ -0,0 +1,347 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace Test\AppFramework\Routing; + +use OC\AppFramework\Routing\RouteParser; +use Symfony\Component\Routing\Route as RoutingRoute; +use Symfony\Component\Routing\RouteCollection; + +class RouteParserTest extends \Test\TestCase { + + protected RouteParser $parser; + + protected function setUp(): void { + $this->parser = new RouteParser(); + } + + public function testParseRoutes(): void { + $routes = ['routes' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], + ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('app1.folders.open')); + $this->assertArrayHasKey('app1.folders.create', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/create', 'POST', 'FoldersController', 'create', route: $collection->get('app1.folders.create')); + } + + public function testParseRoutesRootApps(): void { + $routes = ['routes' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], + ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'core'); + $this->assertArrayHasKey('core.folders.open', $collection->all()); + $this->assertSimpleRoute('/{folderId}/open', 'GET', 'FoldersController', 'open', app: 'core', route: $collection->get('core.folders.open')); + $this->assertArrayHasKey('core.folders.create', $collection->all()); + $this->assertSimpleRoute('/{folderId}/create', 'POST', 'FoldersController', 'create', app: 'core', route: $collection->get('core.folders.create')); + } + + public function testParseRoutesWithResources(): void { + $routes = ['routes' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], + ], 'resources' => [ + 'names' => ['url' => '/names'], + 'folder_names' => ['url' => '/folder/names'], + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.open', $collection->all()); + $this->assertSimpleResource('/apps/app1/folder/names', 'folder_names', 'FolderNamesController', 'app1', $collection); + $this->assertSimpleResource('/apps/app1/names', 'names', 'NamesController', 'app1', $collection); + } + + public function testParseRoutesWithPostfix(): void { + $routes = ['routes' => [ + ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'POST'], + ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'PUT', 'postfix' => '-edit'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.update', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'POST', 'FoldersController', 'update', route: $collection->get('app1.folders.update')); + $this->assertArrayHasKey('app1.folders.update-edit', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'PUT', 'FoldersController', 'update', route: $collection->get('app1.folders.update-edit')); + } + + public function testParseRoutesKebabCaseAction(): void { + $routes = ['routes' => [ + ['name' => 'folders#open_folder', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.open_folder', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'openFolder', route: $collection->get('app1.folders.open_folder')); + } + + public function testParseRoutesKebabCaseController(): void { + $routes = ['routes' => [ + ['name' => 'my_folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.my_folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'MyFoldersController', 'open', route: $collection->get('app1.my_folders.open')); + } + + public function testParseRoutesLowercaseVerb(): void { + $routes = ['routes' => [ + ['name' => 'folders#delete', 'url' => '/{folderId}/delete', 'verb' => 'delete'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.delete', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/delete', 'DELETE', 'FoldersController', 'delete', route: $collection->get('app1.folders.delete')); + } + + public function testParseRoutesMissingVerb(): void { + $routes = ['routes' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open'] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('app1.folders.open')); + } + + public function testParseRoutesWithRequirements(): void { + $routes = ['routes' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'requirements' => ['folderId' => '\d+']] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', requirements: ['folderId' => '\d+'], route: $collection->get('app1.folders.open')); + } + + public function testParseRoutesWithDefaults(): void { + $routes = ['routes' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'defaults' => ['hello' => 'world']] + ]]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertArrayHasKey('app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', defaults: ['hello' => 'world'], route: $collection->get('app1.folders.open')); + } + + public function testParseRoutesInvalidName(): void { + $routes = ['routes' => [ + ['name' => 'folders', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $this->expectException(\UnexpectedValueException::class); + $this->parser->parseDefaultRoutes($routes, 'app1'); + } + + public function testParseRoutesInvalidName2(): void { + $routes = ['routes' => [ + ['name' => 'folders#open#action', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $this->expectException(\UnexpectedValueException::class); + $this->parser->parseDefaultRoutes($routes, 'app1'); + } + + public function testParseRoutesEmpty(): void { + $routes = ['routes' => []]; + + $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); + $this->assertEquals(0, $collection->count()); + } + + // OCS routes + + public function testParseOcsRoutes(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], + ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('ocs.app1.folders.open')); + $this->assertArrayHasKey('ocs.app1.folders.create', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/create', 'POST', 'FoldersController', 'create', route: $collection->get('ocs.app1.folders.create')); + } + + public function testParseOcsRoutesRootApps(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], + ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'core'); + $this->assertArrayHasKey('ocs.core.folders.open', $collection->all()); + $this->assertSimpleRoute('/{folderId}/open', 'GET', 'FoldersController', 'open', app: 'core', route: $collection->get('ocs.core.folders.open')); + $this->assertArrayHasKey('ocs.core.folders.create', $collection->all()); + $this->assertSimpleRoute('/{folderId}/create', 'POST', 'FoldersController', 'create', app: 'core', route: $collection->get('ocs.core.folders.create')); + } + + public function testParseOcsRoutesWithPostfix(): void { + $routes = ['ocs' => [ + ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'POST'], + ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'PUT', 'postfix' => '-edit'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.update', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'POST', 'FoldersController', 'update', route: $collection->get('ocs.app1.folders.update')); + $this->assertArrayHasKey('ocs.app1.folders.update-edit', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'PUT', 'FoldersController', 'update', route: $collection->get('ocs.app1.folders.update-edit')); + } + + public function testParseOcsRoutesKebabCaseAction(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open_folder', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.open_folder', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'openFolder', route: $collection->get('ocs.app1.folders.open_folder')); + } + + public function testParseOcsRoutesKebabCaseController(): void { + $routes = ['ocs' => [ + ['name' => 'my_folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.my_folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'MyFoldersController', 'open', route: $collection->get('ocs.app1.my_folders.open')); + } + + public function testParseOcsRoutesLowercaseVerb(): void { + $routes = ['ocs' => [ + ['name' => 'folders#delete', 'url' => '/{folderId}/delete', 'verb' => 'delete'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.delete', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/delete', 'DELETE', 'FoldersController', 'delete', route: $collection->get('ocs.app1.folders.delete')); + } + + public function testParseOcsRoutesMissingVerb(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open'] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('ocs.app1.folders.open')); + } + + public function testParseOcsRoutesWithRequirements(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'requirements' => ['folderId' => '\d+']] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', requirements: ['folderId' => '\d+'], route: $collection->get('ocs.app1.folders.open')); + } + + public function testParseOcsRoutesWithDefaults(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'defaults' => ['hello' => 'world']] + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); + $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', defaults: ['hello' => 'world'], route: $collection->get('ocs.app1.folders.open')); + } + + public function testParseOcsRoutesInvalidName(): void { + $routes = ['ocs' => [ + ['name' => 'folders', 'url' => '/{folderId}/open', 'verb' => 'GET'] + ]]; + + $this->expectException(\UnexpectedValueException::class); + $this->parser->parseOCSRoutes($routes, 'app1'); + } + + public function testParseOcsRoutesEmpty(): void { + $routes = ['ocs' => []]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertEquals(0, $collection->count()); + } + + public function testParseOcsRoutesWithResources(): void { + $routes = ['ocs' => [ + ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], + ], 'ocs-resources' => [ + 'names' => ['url' => '/names', 'root' => '/core/something'], + 'folder_names' => ['url' => '/folder/names'], + ]]; + + $collection = $this->parser->parseOCSRoutes($routes, 'app1'); + $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); + $this->assertOcsResource('/apps/app1/folder/names', 'folder_names', 'FolderNamesController', 'app1', $collection); + $this->assertOcsResource('/core/something/names', 'names', 'NamesController', 'app1', $collection); + } + + protected function assertSimpleRoute( + string $path, + string $method, + string $controller, + string $action, + string $app = 'app1', + array $requirements = [], + array $defaults = [], + ?RoutingRoute $route = null, + ): void { + self::assertEquals($path, $route->getPath()); + self::assertEqualsCanonicalizing([$method], $route->getMethods()); + self::assertEqualsCanonicalizing($requirements, $route->getRequirements()); + self::assertEquals([...$defaults, 'action' => null, 'caller' => [$app, $controller, $action]], $route->getDefaults()); + } + + protected function assertSimpleResource( + string $path, + string $resourceName, + string $controller, + string $app, + RouteCollection $collection, + ): void { + self::assertArrayHasKey("$app.$resourceName.index", $collection->all()); + self::assertArrayHasKey("$app.$resourceName.show", $collection->all()); + self::assertArrayHasKey("$app.$resourceName.create", $collection->all()); + self::assertArrayHasKey("$app.$resourceName.update", $collection->all()); + self::assertArrayHasKey("$app.$resourceName.destroy", $collection->all()); + + $this->assertSimpleRoute($path, 'GET', $controller, 'index', $app, route: $collection->get("$app.$resourceName.index")); + $this->assertSimpleRoute($path, 'POST', $controller, 'create', $app, route: $collection->get("$app.$resourceName.create")); + $this->assertSimpleRoute("$path/{id}", 'GET', $controller, 'show', $app, route: $collection->get("$app.$resourceName.show")); + $this->assertSimpleRoute("$path/{id}", 'PUT', $controller, 'update', $app, route: $collection->get("$app.$resourceName.update")); + $this->assertSimpleRoute("$path/{id}", 'DELETE', $controller, 'destroy', $app, route: $collection->get("$app.$resourceName.destroy")); + } + + protected function assertOcsResource( + string $path, + string $resourceName, + string $controller, + string $app, + RouteCollection $collection, + ): void { + self::assertArrayHasKey("ocs.$app.$resourceName.index", $collection->all()); + self::assertArrayHasKey("ocs.$app.$resourceName.show", $collection->all()); + self::assertArrayHasKey("ocs.$app.$resourceName.create", $collection->all()); + self::assertArrayHasKey("ocs.$app.$resourceName.update", $collection->all()); + self::assertArrayHasKey("ocs.$app.$resourceName.destroy", $collection->all()); + + $this->assertSimpleRoute($path, 'GET', $controller, 'index', $app, route: $collection->get("ocs.$app.$resourceName.index")); + $this->assertSimpleRoute($path, 'POST', $controller, 'create', $app, route: $collection->get("ocs.$app.$resourceName.create")); + $this->assertSimpleRoute("$path/{id}", 'GET', $controller, 'show', $app, route: $collection->get("ocs.$app.$resourceName.show")); + $this->assertSimpleRoute("$path/{id}", 'PUT', $controller, 'update', $app, route: $collection->get("ocs.$app.$resourceName.update")); + $this->assertSimpleRoute("$path/{id}", 'DELETE', $controller, 'destroy', $app, route: $collection->get("ocs.$app.$resourceName.destroy")); + } +} diff --git a/tests/lib/AppFramework/Services/AppConfigTest.php b/tests/lib/AppFramework/Services/AppConfigTest.php new file mode 100644 index 00000000000..38fa6bdcac6 --- /dev/null +++ b/tests/lib/AppFramework/Services/AppConfigTest.php @@ -0,0 +1,668 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\AppFramework\Services; + +use OC\AppConfig as AppConfigCore; +use OC\AppFramework\Services\AppConfig; +use OCP\Exceptions\AppConfigTypeConflictException; +use OCP\Exceptions\AppConfigUnknownKeyException; +use OCP\IAppConfig as IAppConfigCore; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class AppConfigTest extends TestCase { + private IConfig|MockObject $config; + private IAppConfigCore|MockObject $appConfigCore; + private AppConfig $appConfig; + + private const TEST_APPID = 'appconfig-test'; + + protected function setUp(): void { + parent::setUp(); + $this->config = $this->createMock(IConfig::class); + $this->appConfigCore = $this->createMock(AppConfigCore::class); + + $this->appConfig = new AppConfig($this->config, $this->appConfigCore, self::TEST_APPID); + } + + public function testGetAppKeys(): void { + $expected = ['key1', 'key2', 'key3', 'key4', 'key5', 'key6', 'key7', 'test8']; + $this->appConfigCore->expects($this->once()) + ->method('getKeys') + ->with(self::TEST_APPID) + ->willReturn($expected); + $this->assertSame($expected, $this->appConfig->getAppKeys()); + } + + + /** + * @return array + * @see testHasAppKey + */ + public static function providerHasAppKey(): array { + return [ + // lazy, expected + [false, true], + [true, true], + [false, false], + [true, false], + ]; + } + + /** + * + * @param bool $lazy + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerHasAppKey')] + public function testHasAppKey(bool $lazy, bool $expected): void { + $key = 'key'; + $this->appConfigCore->expects($this->once()) + ->method('hasKey') + ->with(self::TEST_APPID, $key, $lazy) + ->willReturn($expected); + $this->assertSame($expected, $this->appConfig->hasAppKey($key, $lazy)); + } + + + /** + * @return array + * @see testIsSensitive + */ + public static function providerIsSensitive(): array { + return [ + // lazy, expected + [false, true], + [true, true], + [false, false], + [true, false], + ]; + } + + /** + * + * @param bool $lazy + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerIsSensitive')] + public function testIsSensitive(bool $lazy, bool $expected): void { + $key = 'key'; + $this->appConfigCore->expects($this->once()) + ->method('isSensitive') + ->with(self::TEST_APPID, $key, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->isSensitive($key, $lazy)); + } + + /** + * + * @param bool $lazy + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerIsSensitive')] + public function testIsSensitiveException(bool $lazy, bool $expected): void { + $key = 'unknown-key'; + $this->appConfigCore->expects($this->once()) + ->method('isSensitive') + ->with(self::TEST_APPID, $key, $lazy) + ->willThrowException(new AppConfigUnknownKeyException()); + + $this->expectException(AppConfigUnknownKeyException::class); + $this->appConfig->isSensitive($key, $lazy); + } + + /** + * @return array + * @see testIsLazy + */ + public static function providerIsLazy(): array { + return [ + // expected + [true], + [false], + ]; + } + + /** + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerIsLazy')] + public function testIsLazy(bool $expected): void { + $key = 'key'; + $this->appConfigCore->expects($this->once()) + ->method('isLazy') + ->with(self::TEST_APPID, $key) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->isLazy($key)); + } + + public function testIsLazyException(): void { + $key = 'unknown-key'; + $this->appConfigCore->expects($this->once()) + ->method('isLazy') + ->with(self::TEST_APPID, $key) + ->willThrowException(new AppConfigUnknownKeyException()); + + $this->expectException(AppConfigUnknownKeyException::class); + $this->appConfig->isLazy($key); + } + + /** + * @return array + * @see testGetAllAppValues + */ + public static function providerGetAllAppValues(): array { + return [ + // key, filtered + ['', false], + ['', true], + ['key', false], + ['key', true], + ]; + } + + /** + * + * @param string $key + * @param bool $filtered + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAllAppValues')] + public function testGetAllAppValues(string $key, bool $filtered): void { + $expected = [ + 'key1' => 'value1', + 'key2' => 3, + 'key3' => 3.14, + 'key4' => true + ]; + + $this->appConfigCore->expects($this->once()) + ->method('getAllValues') + ->with(self::TEST_APPID, $key, $filtered) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->getAllAppValues($key, $filtered)); + } + + public function testSetAppValue(): void { + $key = 'key'; + $value = 'value'; + $this->appConfigCore->expects($this->once()) + ->method('setValueMixed') + ->with(self::TEST_APPID, $key, $value); + + $this->appConfig->setAppValue($key, $value); + } + + /** + * @return array + * @see testSetAppValueString + * @see testSetAppValueStringException + * @see testSetAppValueInt + * @see testSetAppValueIntException + * @see testSetAppValueFloat + * @see testSetAppValueFloatException + * @see testSetAppValueArray + * @see testSetAppValueArrayException + */ + public static function providerSetAppValue(): array { + return [ + // lazy, sensitive, expected + [false, false, true], + [false, true, true], + [true, true, true], + [true, false, true], + [false, false, false], + [false, true, false], + [true, true, false], + [true, false, false], + ]; + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueString(bool $lazy, bool $sensitive, bool $expected): void { + $key = 'key'; + $value = 'valueString'; + $this->appConfigCore->expects($this->once()) + ->method('setValueString') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->setAppValueString($key, $value, $lazy, $sensitive)); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueStringException(bool $lazy, bool $sensitive): void { + $key = 'key'; + $value = 'valueString'; + $this->appConfigCore->expects($this->once()) + ->method('setValueString') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->setAppValueString($key, $value, $lazy, $sensitive); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueInt(bool $lazy, bool $sensitive, bool $expected): void { + $key = 'key'; + $value = 42; + $this->appConfigCore->expects($this->once()) + ->method('setValueInt') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->setAppValueInt($key, $value, $lazy, $sensitive)); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueIntException(bool $lazy, bool $sensitive): void { + $key = 'key'; + $value = 42; + $this->appConfigCore->expects($this->once()) + ->method('setValueInt') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->setAppValueInt($key, $value, $lazy, $sensitive); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueFloat(bool $lazy, bool $sensitive, bool $expected): void { + $key = 'key'; + $value = 3.14; + $this->appConfigCore->expects($this->once()) + ->method('setValueFloat') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->setAppValueFloat($key, $value, $lazy, $sensitive)); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueFloatException(bool $lazy, bool $sensitive): void { + $key = 'key'; + $value = 3.14; + $this->appConfigCore->expects($this->once()) + ->method('setValueFloat') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->setAppValueFloat($key, $value, $lazy, $sensitive); + } + + /** + * @return array + * @see testSetAppValueBool + */ + public static function providerSetAppValueBool(): array { + return [ + // lazy, expected + [false, true], + [false, false], + [true, true], + [true, false], + ]; + } + + /** + * + * @param bool $lazy + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValueBool')] + public function testSetAppValueBool(bool $lazy, bool $expected): void { + $key = 'key'; + $value = true; + $this->appConfigCore->expects($this->once()) + ->method('setValueBool') + ->with(self::TEST_APPID, $key, $value, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->setAppValueBool($key, $value, $lazy)); + } + + /** + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValueBool')] + public function testSetAppValueBoolException(bool $lazy): void { + $key = 'key'; + $value = true; + $this->appConfigCore->expects($this->once()) + ->method('setValueBool') + ->with(self::TEST_APPID, $key, $value, $lazy) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->setAppValueBool($key, $value, $lazy); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueArray(bool $lazy, bool $sensitive, bool $expected): void { + $key = 'key'; + $value = ['item' => true]; + $this->appConfigCore->expects($this->once()) + ->method('setValueArray') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->setAppValueArray($key, $value, $lazy, $sensitive)); + } + + /** + * + * @param bool $lazy + * @param bool $sensitive + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] + public function testSetAppValueArrayException(bool $lazy, bool $sensitive): void { + $key = 'key'; + $value = ['item' => true]; + $this->appConfigCore->expects($this->once()) + ->method('setValueArray') + ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->setAppValueArray($key, $value, $lazy, $sensitive); + } + + public function testGetAppValue(): void { + $key = 'key'; + $value = 'value'; + $default = 'default'; + $this->appConfigCore->expects($this->once()) + ->method('getValueMixed') + ->with(self::TEST_APPID, $key, $default) + ->willReturn($value); + + $this->assertSame($value, $this->appConfig->getAppValue($key, $default)); + } + + public function testGetAppValueDefault(): void { + $key = 'key'; + $default = 'default'; + $this->appConfigCore->expects($this->once()) + ->method('getValueMixed') + ->with(self::TEST_APPID, $key, $default) + ->willReturn($default); + + $this->assertSame($default, $this->appConfig->getAppValue($key, $default)); + } + + /** + * @return array + * @see testGetAppValueString + * @see testGetAppValueStringException + * @see testGetAppValueInt + * @see testGetAppValueIntException + * @see testGetAppValueFloat + * @see testGetAppValueFloatException + * @see testGetAppValueBool + * @see testGetAppValueBoolException + * @see testGetAppValueArray + * @see testGetAppValueArrayException + */ + public static function providerGetAppValue(): array { + return [ + // lazy, exist + [false, false], + [false, true], + [true, true], + [true, false] + ]; + } + + /** + * + * @param bool $lazy + * @param bool $exist + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueString(bool $lazy, bool $exist): void { + $key = 'key'; + $value = 'valueString'; + $default = 'default'; + + $expected = ($exist) ? $value : $default; + $this->appConfigCore->expects($this->once()) + ->method('getValueString') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->getAppValueString($key, $default, $lazy)); + } + + /** + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueStringException(bool $lazy): void { + $key = 'key'; + $default = 'default'; + + $this->appConfigCore->expects($this->once()) + ->method('getValueString') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->getAppValueString($key, $default, $lazy); + } + + /** + * + * @param bool $lazy + * @param bool $exist + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueInt(bool $lazy, bool $exist): void { + $key = 'key'; + $value = 42; + $default = 17; + + $expected = ($exist) ? $value : $default; + $this->appConfigCore->expects($this->once()) + ->method('getValueInt') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->getAppValueInt($key, $default, $lazy)); + } + + /** + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueIntException(bool $lazy): void { + $key = 'key'; + $default = 17; + + $this->appConfigCore->expects($this->once()) + ->method('getValueInt') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->getAppValueInt($key, $default, $lazy); + } + + /** + * + * @param bool $lazy + * @param bool $exist + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueFloat(bool $lazy, bool $exist): void { + $key = 'key'; + $value = 3.14; + $default = 17.04; + + $expected = ($exist) ? $value : $default; + $this->appConfigCore->expects($this->once()) + ->method('getValueFloat') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->getAppValueFloat($key, $default, $lazy)); + } + + /** + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueFloatException(bool $lazy): void { + $key = 'key'; + $default = 17.04; + + $this->appConfigCore->expects($this->once()) + ->method('getValueFloat') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->getAppValueFloat($key, $default, $lazy); + } + + /** + * + * @param bool $lazy + * @param bool $exist + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueBool(bool $lazy, bool $exist): void { + $key = 'key'; + $value = true; + $default = false; + + $expected = ($exist) ? $value : $default; // yes, it can be simplified + $this->appConfigCore->expects($this->once()) + ->method('getValueBool') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->getAppValueBool($key, $default, $lazy)); + } + + /** + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueBoolException(bool $lazy): void { + $key = 'key'; + $default = false; + + $this->appConfigCore->expects($this->once()) + ->method('getValueBool') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->getAppValueBool($key, $default, $lazy); + } + + /** + * + * @param bool $lazy + * @param bool $exist + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueArray(bool $lazy, bool $exist): void { + $key = 'key'; + $value = ['item' => true]; + $default = []; + + $expected = ($exist) ? $value : $default; + $this->appConfigCore->expects($this->once()) + ->method('getValueArray') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willReturn($expected); + + $this->assertSame($expected, $this->appConfig->getAppValueArray($key, $default, $lazy)); + } + + /** + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] + public function testGetAppValueArrayException(bool $lazy): void { + $key = 'key'; + $default = []; + + $this->appConfigCore->expects($this->once()) + ->method('getValueArray') + ->with(self::TEST_APPID, $key, $default, $lazy) + ->willThrowException(new AppConfigTypeConflictException()); + + $this->expectException(AppConfigTypeConflictException::class); + $this->appConfig->getAppValueArray($key, $default, $lazy); + } + + public function testDeleteAppValue(): void { + $key = 'key'; + $this->appConfigCore->expects($this->once()) + ->method('deleteKey') + ->with(self::TEST_APPID, $key); + + $this->appConfig->deleteAppValue($key); + } + + public function testDeleteAppValues(): void { + $this->appConfigCore->expects($this->once()) + ->method('deleteApp') + ->with(self::TEST_APPID); + + $this->appConfig->deleteAppValues(); + } +} diff --git a/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php b/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php new file mode 100644 index 00000000000..00ae4792824 --- /dev/null +++ b/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php @@ -0,0 +1,253 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Utility; + +use OC\AppFramework\Utility\ControllerMethodReflector; + +class BaseController { + /** + * @Annotation + */ + public function test() { + } + + /** + * @Annotation + */ + public function test2() { + } + + /** + * @Annotation + */ + public function test3() { + } +} + +class MiddleController extends BaseController { + /** + * @NoAnnotation + */ + public function test2() { + } + + public function test3() { + } + + /** + * @psalm-param int<-4, 42> $rangedOne + * @psalm-param int<min, max> $rangedTwo + * @psalm-param int<1, 6>|null $rangedThree + * @psalm-param ?int<-70, -30> $rangedFour + * @return void + */ + public function test4(int $rangedOne, int $rangedTwo, ?int $rangedThree, ?int $rangedFour) { + } +} + +class EndController extends MiddleController { +} + +class ControllerMethodReflectorTest extends \Test\TestCase { + /** + * @Annotation + */ + public function testReadAnnotation(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'testReadAnnotation' + ); + + $this->assertTrue($reader->hasAnnotation('Annotation')); + } + + /** + * @Annotation(parameter=value) + */ + public function testGetAnnotationParameterSingle(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + self::class, + __FUNCTION__ + ); + + $this->assertSame('value', $reader->getAnnotationParameter('Annotation', 'parameter')); + } + + /** + * @Annotation(parameter1=value1, parameter2=value2,parameter3=value3) + */ + public function testGetAnnotationParameterMultiple(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + self::class, + __FUNCTION__ + ); + + $this->assertSame('value1', $reader->getAnnotationParameter('Annotation', 'parameter1')); + $this->assertSame('value2', $reader->getAnnotationParameter('Annotation', 'parameter2')); + $this->assertSame('value3', $reader->getAnnotationParameter('Annotation', 'parameter3')); + } + + /** + * @Annotation + * @param test + */ + public function testReadAnnotationNoLowercase(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'testReadAnnotationNoLowercase' + ); + + $this->assertTrue($reader->hasAnnotation('Annotation')); + $this->assertFalse($reader->hasAnnotation('param')); + } + + + /** + * @Annotation + * @param int $test + */ + public function testReadTypeIntAnnotations(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'testReadTypeIntAnnotations' + ); + + $this->assertEquals('int', $reader->getType('test')); + } + + /** + * @Annotation + * @param int $a + * @param int $b + */ + public function arguments3($a, float $b, int $c, $d) { + } + + /** + * @requires PHP 7 + */ + public function testReadTypeIntAnnotationsScalarTypes(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'arguments3' + ); + + $this->assertEquals('int', $reader->getType('a')); + $this->assertEquals('float', $reader->getType('b')); + $this->assertEquals('int', $reader->getType('c')); + $this->assertNull($reader->getType('d')); + } + + + /** + * @Annotation + * @param double $test something special + */ + public function testReadTypeDoubleAnnotations(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'testReadTypeDoubleAnnotations' + ); + + $this->assertEquals('double', $reader->getType('test')); + } + + /** + * @Annotation + * @param string $foo + */ + public function testReadTypeWhitespaceAnnotations(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'testReadTypeWhitespaceAnnotations' + ); + + $this->assertEquals('string', $reader->getType('foo')); + } + + + public function arguments($arg, $arg2 = 'hi') { + } + public function testReflectParameters(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'arguments' + ); + + $this->assertEquals(['arg' => null, 'arg2' => 'hi'], $reader->getParameters()); + } + + + public function arguments2($arg) { + } + public function testReflectParameters2(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect( + '\Test\AppFramework\Utility\ControllerMethodReflectorTest', + 'arguments2' + ); + + $this->assertEquals(['arg' => null], $reader->getParameters()); + } + + + public function testInheritance(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect('Test\AppFramework\Utility\EndController', 'test'); + + $this->assertTrue($reader->hasAnnotation('Annotation')); + } + + + public function testInheritanceOverride(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect('Test\AppFramework\Utility\EndController', 'test2'); + + $this->assertTrue($reader->hasAnnotation('NoAnnotation')); + $this->assertFalse($reader->hasAnnotation('Annotation')); + } + + + public function testInheritanceOverrideNoDocblock(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect('Test\AppFramework\Utility\EndController', 'test3'); + + $this->assertFalse($reader->hasAnnotation('Annotation')); + } + + public function testRangeDetection(): void { + $reader = new ControllerMethodReflector(); + $reader->reflect('Test\AppFramework\Utility\EndController', 'test4'); + + $rangeInfo1 = $reader->getRange('rangedOne'); + $this->assertSame(-4, $rangeInfo1['min']); + $this->assertSame(42, $rangeInfo1['max']); + + $rangeInfo2 = $reader->getRange('rangedTwo'); + $this->assertSame(PHP_INT_MIN, $rangeInfo2['min']); + $this->assertSame(PHP_INT_MAX, $rangeInfo2['max']); + + $rangeInfo3 = $reader->getRange('rangedThree'); + $this->assertSame(1, $rangeInfo3['min']); + $this->assertSame(6, $rangeInfo3['max']); + + $rangeInfo3 = $reader->getRange('rangedFour'); + $this->assertSame(-70, $rangeInfo3['min']); + $this->assertSame(-30, $rangeInfo3['max']); + } +} diff --git a/tests/lib/AppFramework/Utility/SimpleContainerTest.php b/tests/lib/AppFramework/Utility/SimpleContainerTest.php new file mode 100644 index 00000000000..33800c7376f --- /dev/null +++ b/tests/lib/AppFramework/Utility/SimpleContainerTest.php @@ -0,0 +1,260 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\AppFramework\Utility; + +use OC\AppFramework\Utility\SimpleContainer; +use OCP\AppFramework\QueryException; +use Psr\Container\NotFoundExceptionInterface; + +interface TestInterface { +} + +class ClassEmptyConstructor implements IInterfaceConstructor { +} + +class ClassSimpleConstructor implements IInterfaceConstructor { + public function __construct( + public $test, + ) { + } +} + +class ClassComplexConstructor { + public function __construct( + public ClassSimpleConstructor $class, + public $test, + ) { + } +} + +class ClassNullableUntypedConstructorArg { + public function __construct( + public $class, + ) { + } +} +class ClassNullableTypedConstructorArg { + public function __construct( + public ?\Some\Class $class, + ) { + } +} + +interface IInterfaceConstructor { +} +class ClassInterfaceConstructor { + public function __construct( + public IInterfaceConstructor $class, + public $test, + ) { + } +} + + +class SimpleContainerTest extends \Test\TestCase { + private $container; + + protected function setUp(): void { + $this->container = new SimpleContainer(); + } + + + + public function testRegister(): void { + $this->container->registerParameter('test', 'abc'); + $this->assertEquals('abc', $this->container->query('test')); + } + + + /** + * Test querying a class that is not registered without autoload enabled + */ + public function testNothingRegistered(): void { + try { + $this->container->query('something really hard', false); + $this->fail('Expected `QueryException` exception was not thrown'); + } catch (\Throwable $exception) { + $this->assertInstanceOf(QueryException::class, $exception); + $this->assertInstanceOf(NotFoundExceptionInterface::class, $exception); + } + } + + + /** + * Test querying a class that is not registered with autoload enabled + */ + public function testNothingRegistered_autoload(): void { + try { + $this->container->query('something really hard'); + $this->fail('Expected `QueryException` exception was not thrown'); + } catch (\Throwable $exception) { + $this->assertInstanceOf(QueryException::class, $exception); + $this->assertInstanceOf(NotFoundExceptionInterface::class, $exception); + } + } + + + + public function testNotAClass(): void { + $this->expectException(QueryException::class); + + $this->container->query('Test\AppFramework\Utility\TestInterface'); + } + + + public function testNoConstructorClass(): void { + $object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + $this->assertTrue($object instanceof ClassEmptyConstructor); + } + + + public function testInstancesOnlyOnce(): void { + $object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + $object2 = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + $this->assertSame($object, $object2); + } + + public function testConstructorSimple(): void { + $this->container->registerParameter('test', 'abc'); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassSimpleConstructor' + ); + $this->assertTrue($object instanceof ClassSimpleConstructor); + $this->assertEquals('abc', $object->test); + } + + + public function testConstructorComplex(): void { + $this->container->registerParameter('test', 'abc'); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassComplexConstructor' + ); + $this->assertTrue($object instanceof ClassComplexConstructor); + $this->assertEquals('abc', $object->class->test); + $this->assertEquals('abc', $object->test); + } + + + public function testConstructorComplexInterface(): void { + $this->container->registerParameter('test', 'abc'); + $this->container->registerService( + 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { + return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor'); + }); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassInterfaceConstructor' + ); + $this->assertTrue($object instanceof ClassInterfaceConstructor); + $this->assertEquals('abc', $object->class->test); + $this->assertEquals('abc', $object->test); + } + + + public function testOverrideService(): void { + $this->container->registerService( + 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { + return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor'); + }); + $this->container->registerService( + 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { + return $c->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + }); + $object = $this->container->query( + 'Test\AppFramework\Utility\IInterfaceConstructor' + ); + $this->assertTrue($object instanceof ClassEmptyConstructor); + } + + public function testRegisterAliasParamter(): void { + $this->container->registerParameter('test', 'abc'); + $this->container->registerAlias('test1', 'test'); + $this->assertEquals('abc', $this->container->query('test1')); + } + + public function testRegisterAliasService(): void { + $this->container->registerService('test', function () { + return new \StdClass; + }, true); + $this->container->registerAlias('test1', 'test'); + $this->assertSame( + $this->container->query('test'), $this->container->query('test')); + $this->assertSame( + $this->container->query('test1'), $this->container->query('test1')); + $this->assertSame( + $this->container->query('test'), $this->container->query('test1')); + } + + public static function sanitizeNameProvider(): array { + return [ + ['ABC\\Foo', 'ABC\\Foo'], + ['\\ABC\\Foo', '\\ABC\\Foo'], + ['\\ABC\\Foo', 'ABC\\Foo'], + ['ABC\\Foo', '\\ABC\\Foo'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sanitizeNameProvider')] + public function testSanitizeName($register, $query): void { + $this->container->registerService($register, function () { + return 'abc'; + }); + $this->assertEquals('abc', $this->container->query($query)); + } + + + public function testConstructorComplexNoTestParameterFound(): void { + $this->expectException(QueryException::class); + + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassComplexConstructor' + ); + /* Use the object to trigger DI on PHP >= 8.4 */ + get_object_vars($object); + } + + public function testRegisterFactory(): void { + $this->container->registerService('test', function () { + return new \StdClass(); + }, false); + $this->assertNotSame( + $this->container->query('test'), $this->container->query('test')); + } + + public function testRegisterAliasFactory(): void { + $this->container->registerService('test', function () { + return new \StdClass(); + }, false); + $this->container->registerAlias('test1', 'test'); + $this->assertNotSame( + $this->container->query('test'), $this->container->query('test')); + $this->assertNotSame( + $this->container->query('test1'), $this->container->query('test1')); + $this->assertNotSame( + $this->container->query('test'), $this->container->query('test1')); + } + + public function testQueryUntypedNullable(): void { + $this->expectException(QueryException::class); + + $object = $this->container->query( + ClassNullableUntypedConstructorArg::class + ); + /* Use the object to trigger DI on PHP >= 8.4 */ + get_object_vars($object); + } + + public function testQueryTypedNullable(): void { + /** @var ClassNullableTypedConstructorArg $service */ + $service = $this->container->query(ClassNullableTypedConstructorArg::class); + + self::assertNull($service->class); + } +} diff --git a/tests/lib/AppFramework/Utility/TimeFactoryTest.php b/tests/lib/AppFramework/Utility/TimeFactoryTest.php new file mode 100644 index 00000000000..276b2d6da4f --- /dev/null +++ b/tests/lib/AppFramework/Utility/TimeFactoryTest.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\AppFramework\Utility; + +use OC\AppFramework\Utility\TimeFactory; + +class TimeFactoryTest extends \Test\TestCase { + protected TimeFactory $timeFactory; + + protected function setUp(): void { + $this->timeFactory = new TimeFactory(); + } + + public function testNow(): void { + $now = $this->timeFactory->now(); + self::assertSame('UTC', $now->getTimezone()->getName()); + } + + public function testNowWithTimeZone(): void { + $timezone = new \DateTimeZone('Europe/Berlin'); + $withTimeZone = $this->timeFactory->withTimeZone($timezone); + + $now = $withTimeZone->now(); + self::assertSame('Europe/Berlin', $now->getTimezone()->getName()); + } + + public function testGetTimeZone(): void { + $expected = new \DateTimeZone('Europe/Berlin'); + $actual = $this->timeFactory->getTimeZone('Europe/Berlin'); + self::assertEquals($expected, $actual); + } + + public function testGetTimeZoneUTC(): void { + $expected = new \DateTimeZone('UTC'); + $actual = $this->timeFactory->getTimeZone(); + self::assertEquals($expected, $actual); + } + + public function testGetTimeZoneInvalid(): void { + $this->expectException(\Exception::class); + $this->timeFactory->getTimeZone('blubblub'); + } +} |