aboutsummaryrefslogtreecommitdiffstats
path: root/tests/lib/AppFramework
diff options
context:
space:
mode:
Diffstat (limited to 'tests/lib/AppFramework')
-rw-r--r--tests/lib/AppFramework/AppTest.php220
-rw-r--r--tests/lib/AppFramework/Bootstrap/BootContextTest.php51
-rw-r--r--tests/lib/AppFramework/Bootstrap/CoordinatorTest.php119
-rw-r--r--tests/lib/AppFramework/Bootstrap/FunctionInjectorTest.php68
-rw-r--r--tests/lib/AppFramework/Bootstrap/RegistrationContextTest.php173
-rw-r--r--tests/lib/AppFramework/Controller/ApiControllerTest.php43
-rw-r--r--tests/lib/AppFramework/Controller/AuthPublicShareControllerTest.php142
-rw-r--r--tests/lib/AppFramework/Controller/ControllerTest.php147
-rw-r--r--tests/lib/AppFramework/Controller/OCSControllerTest.php135
-rw-r--r--tests/lib/AppFramework/Controller/PublicShareControllerTest.php85
-rw-r--r--tests/lib/AppFramework/Db/EntityTest.php320
-rw-r--r--tests/lib/AppFramework/Db/QBMapperDBTest.php160
-rw-r--r--tests/lib/AppFramework/Db/QBMapperTest.php245
-rw-r--r--tests/lib/AppFramework/Db/TransactionalTest.php79
-rw-r--r--tests/lib/AppFramework/DependencyInjection/DIContainerTest.php144
-rw-r--r--tests/lib/AppFramework/DependencyInjection/DIIntergrationTests.php121
-rw-r--r--tests/lib/AppFramework/Http/ContentSecurityPolicyTest.php516
-rw-r--r--tests/lib/AppFramework/Http/DataResponseTest.php75
-rw-r--r--tests/lib/AppFramework/Http/DispatcherTest.php582
-rw-r--r--tests/lib/AppFramework/Http/DownloadResponseTest.php48
-rw-r--r--tests/lib/AppFramework/Http/EmptyContentSecurityPolicyTest.php498
-rw-r--r--tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php116
-rw-r--r--tests/lib/AppFramework/Http/FeaturePolicyTest.php116
-rw-r--r--tests/lib/AppFramework/Http/FileDisplayResponseTest.php94
-rw-r--r--tests/lib/AppFramework/Http/HttpTest.php48
-rw-r--r--tests/lib/AppFramework/Http/JSONResponseTest.php98
-rw-r--r--tests/lib/AppFramework/Http/OutputTest.php30
-rw-r--r--tests/lib/AppFramework/Http/PublicTemplateResponseTest.php62
-rw-r--r--tests/lib/AppFramework/Http/RedirectResponseTest.php37
-rw-r--r--tests/lib/AppFramework/Http/RequestIdTest.php57
-rw-r--r--tests/lib/AppFramework/Http/RequestStream.php116
-rw-r--r--tests/lib/AppFramework/Http/RequestTest.php2262
-rw-r--r--tests/lib/AppFramework/Http/ResponseTest.php277
-rw-r--r--tests/lib/AppFramework/Http/StreamResponseTest.php82
-rw-r--r--tests/lib/AppFramework/Http/TemplateResponseTest.php69
-rw-r--r--tests/lib/AppFramework/Middleware/AdditionalScriptsMiddlewareTest.php114
-rw-r--r--tests/lib/AppFramework/Middleware/CompressionMiddlewareTest.php145
-rw-r--r--tests/lib/AppFramework/Middleware/MiddlewareDispatcherTest.php282
-rw-r--r--tests/lib/AppFramework/Middleware/MiddlewareTest.php79
-rw-r--r--tests/lib/AppFramework/Middleware/NotModifiedMiddlewareTest.php87
-rw-r--r--tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php184
-rw-r--r--tests/lib/AppFramework/Middleware/PublicShare/PublicShareMiddlewareTest.php273
-rw-r--r--tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php328
-rw-r--r--tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php344
-rw-r--r--tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php122
-rw-r--r--tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php69
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php145
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php17
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php15
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php38
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php171
-rw-r--r--tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php209
-rw-r--r--tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php322
-rw-r--r--tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php120
-rw-r--r--tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php701
-rw-r--r--tests/lib/AppFramework/Middleware/SessionMiddlewareTest.php138
-rw-r--r--tests/lib/AppFramework/OCS/BaseResponseTest.php81
-rw-r--r--tests/lib/AppFramework/OCS/V2ResponseTest.php36
-rw-r--r--tests/lib/AppFramework/Routing/RouteParserTest.php347
-rw-r--r--tests/lib/AppFramework/Services/AppConfigTest.php668
-rw-r--r--tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php253
-rw-r--r--tests/lib/AppFramework/Utility/SimpleContainerTest.php260
-rw-r--r--tests/lib/AppFramework/Utility/TimeFactoryTest.php50
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/../&amp;/&?someQueryParameter=QueryParam', 'index.php', '/apps/files/../&amp;/&'],
+ ['/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');
+ }
+}