aboutsummaryrefslogtreecommitdiffstats
path: root/tests/unit/Module
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/Module')
-rw-r--r--tests/unit/Module/AdminAccountEditTest.php222
-rw-r--r--tests/unit/Module/AdminAccountsTest.php173
-rw-r--r--tests/unit/Module/HelpTest.php198
-rw-r--r--tests/unit/Module/ItemTest.php56
-rw-r--r--tests/unit/Module/MagicTest.php120
-rw-r--r--tests/unit/Module/OwaTest.php64
-rw-r--r--tests/unit/Module/RbmarkTest.php80
-rw-r--r--tests/unit/Module/RpostTest.php81
-rw-r--r--tests/unit/Module/SetupTest.php76
-rw-r--r--tests/unit/Module/TestCase.php216
10 files changed, 1286 insertions, 0 deletions
diff --git a/tests/unit/Module/AdminAccountEditTest.php b/tests/unit/Module/AdminAccountEditTest.php
new file mode 100644
index 000000000..818f30f26
--- /dev/null
+++ b/tests/unit/Module/AdminAccountEditTest.php
@@ -0,0 +1,222 @@
+<?php
+/* Tests for Account_edit module
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+use DateTimeImmutable;
+use PHPUnit\Framework\Attributes\{Before, After};
+
+class AdminAccountEditTest extends TestCase {
+
+ private $stub_is_site_admin;
+ private $stub_info;
+ private $stub_notice;
+ private $stub_check_security;
+ private $stub_get_form_security_token;
+
+ private array $info;
+ private array $notice;
+
+ #[Before]
+ public function setup_mocks(): void {
+ /*
+ * As we're testing pages that should only be reachable by the
+ * site admin, it makes no sense to have it return anything else
+ * than true.
+ */
+ $this->stub_is_site_admin =
+ $this->getFunctionMock('Zotlabs\Module', 'is_site_admin')
+ ->expects($this->once())
+ ->willReturn(true);
+
+ $this->info = [];
+ $this->stub_info =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'info')
+ ->expects($this->any())
+ ->willReturnCallback(function (string $arg) {
+ $this->info[] = $arg;
+ });
+
+ $this->notice = [];
+ $this->stub_notice =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'notice')
+ ->expects($this->any())
+ ->willReturnCallback(function (string $arg) {
+ $this->notice[] = $arg;
+ });
+
+ }
+
+ #[After]
+ public function tear_down_mocks(): void {
+ $this->stub_is_site_admin = null;
+ $this->stub_info = null;
+ $this->stub_notice = null;
+ $this->stub_check_security = null;
+ $this->stub_get_form_security_token = null;
+ }
+
+ public function test_rendering_admin_account_edit_page(): void {
+ $this->stub_get_form_security_token =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'get_form_security_token')
+ ->expects($this->once())
+ ->willReturn('the-csrf-token');
+
+ $account = $this->fixtures['account'][0];
+
+ $this->get("admin/account_edit/{$account['account_id']}");
+
+ $this->assertPageContains("<form action=\"admin/account_edit/{$account['account_id']}\" method=\"post\"");
+ $this->assertPageContains($account['account_email']);
+
+ // Check that we generate a CSRF token for the form
+ $this->assertPageContains("<input type=\"hidden\" name=\"security\" value=\"the-csrf-token\"");
+ }
+
+ public function test_rendering_admin_account_edit_page_fails_if_id_is_not_found(): void {
+ $this->get("admin/account_edit/666");
+
+ $this->assertEquals('', \App::$page['content']);
+ }
+
+ public function test_rendering_admin_account_edit_page_fails_if_id_is_not_numeric(): void {
+ $this->get("admin/account_edit/66invalid");
+
+ $this->assertEquals('', \App::$page['content']);
+ }
+
+ public function test_post_empty_form_does_not_modify_account(): void {
+ $this->stub_goaway();
+ $this->stub_check_form_security(true);
+
+ $account = get_account_by_id($this->fixtures['account'][0]['account_id']);
+
+ try {
+ $this->post(
+ "admin/account_edit/{$account['account_id']}",
+ [],
+ [
+ 'aid' => $account['account_id'],
+ 'pass1' => '',
+ 'pass2' => '',
+ 'service_class' => $account['account_service_class'],
+ 'account_language' => $account['account_language'],
+ 'security' => 'The security token',
+ ]
+ );
+ } catch (RedirectException $ex) {
+ $this->assertEquals(z_root() . '/admin/accounts', $ex->getMessage());
+ }
+
+ $reloaded = get_account_by_id($account['account_id']);
+
+ $this->assertEquals($account, $reloaded);
+
+ // Not sure if this is expected behaviour, but this is how it is today.
+ $this->assertContains('Account settings updated.' . EOL, $this->info);
+ }
+
+ public function test_post_form_changes_account(): void {
+ $this->stub_goaway();
+ $this->stub_check_form_security(true);
+
+ // clone account from fixture, to ensure it's not replaced with
+ // the reloaded one below.
+ $account = get_account_by_id($this->fixtures['account'][0]['account_id']);
+
+ try {
+ $this->post(
+ "admin/account_edit/{$account['account_id']}",
+ [],
+ [
+ 'aid' => $account['account_id'],
+ 'pass1' => 'hunter2',
+ 'pass2' => 'hunter2',
+ 'service_class' => 'Some other class',
+ 'account_language' => 'nn',
+ 'security' => 'The security token',
+ ]
+ );
+ } catch (RedirectException $ex) {
+ $this->assertEquals(z_root() . '/admin/accounts', $ex->getMessage());
+ }
+
+ $reloaded = get_account_by_id($account['account_id']);
+
+ $this->assertNotEquals($account, $reloaded);
+ $this->assertEquals('Some other class', $reloaded['account_service_class']);
+ $this->assertEquals('nn', $reloaded['account_language']);
+
+ $now = new DateTimeImmutable('now');
+ $this->assertEquals($now->format('Y-m-d H:i:s'), $reloaded['account_password_changed']);
+
+ $this->assertContains('Account settings updated.' . EOL, $this->info);
+ $this->assertContains("Password changed for account {$account['account_id']}." . EOL, $this->info);
+ }
+
+ public function test_form_with_missing_or_incalid_csrf_token_is_rejected(): void {
+ $this->expectException(KillmeException::class);
+
+ // Emulate a failed CSRF check
+ $this->stub_check_form_security(false);
+
+ $account_id = $this->fixtures['account'][0]['account_id'];
+
+ $this->post(
+ "admin/account_edit/{$account_id}",
+ [],
+ [
+ 'aid' => $account_id,
+ 'pass1' => 'hunter2',
+ 'pass2' => 'hunter2',
+ 'service_class' => 'Some other class',
+ 'account_language' => 'nn',
+ 'security' => 'Invalid security token',
+ ]
+ );
+ }
+
+ /*
+ * Override the stub_goaway method because we need the stub to live in the
+ * Admin namespace.
+ */
+ protected function stub_goaway(): void {
+ $this->goaway_stub = $this->getFunctionMock('Zotlabs\Module\Admin', 'goaway')
+ ->expects($this->once())
+ ->willReturnCallback(
+ function (string $uri) {
+ throw new RedirectException($uri);
+ }
+ );
+ }
+
+ /**
+ * Stub the check_form_security_token_ForbiddenOnErr.
+ *
+ * In these tests we're not really interested in _how_ the form security
+ * tokens work, but that the code under test perform the checks. This stub
+ * allows us to do that without having to worry if everything is set up so
+ * that the real function would work or not.
+ *
+ * @param bool $valid true if emulating a valid token, false otherwise.
+ */
+ protected function stub_check_form_security(bool $valid): void {
+ $this->stub_check_security =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'check_form_security_token_ForbiddenOnErr')
+ ->expects($this->once())
+ ->with(
+ $this->identicalTo('admin_account_edit'),
+ $this->identicalTo('security'))
+ ->willReturnCallback(function () use ($valid) {
+ if (! $valid) {
+ throw new KillmeException();
+ }
+ });
+ }
+}
diff --git a/tests/unit/Module/AdminAccountsTest.php b/tests/unit/Module/AdminAccountsTest.php
new file mode 100644
index 000000000..2c76f2779
--- /dev/null
+++ b/tests/unit/Module/AdminAccountsTest.php
@@ -0,0 +1,173 @@
+<?php
+/*
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+use PHPUnit\Framework\Attributes\Before;
+
+class AdminAccountsTest extends TestCase {
+
+ protected $stub_check_security;
+ protected $stub_is_site_admin;
+ protected $stub_goaway;
+ protected $stub_notice;
+
+ protected array $notice;
+
+ /**
+ * Set up the stubs common for the tests.
+ */
+ #[Before]
+ public function setup_stubs(): void {
+ $this->stub_check_form_security();
+ $this->stub_is_site_admin();
+ $this->stub_goaway();
+ $this->stub_notice();
+ }
+
+ public function test_blocking_accounts_marks_selected_accounts_as_blocked(): void {
+ $params = [
+ 'user' => [ 42 ],
+ 'blocked' => [ false ],
+ 'page_accounts_block' => true,
+ ];
+
+ try {
+ $this->post('admin/accounts', [], $params);
+ } catch (RedirectException $redirect) {
+ $this->assertEquals(z_root() . '/admin/accounts', $redirect->getMessage());
+ }
+
+ $account = get_account_by_id(42);
+ $this->assertEquals(ACCOUNT_BLOCKED, $account['account_flags'] & ACCOUNT_BLOCKED);
+
+ $this->assertEquals('1 account blocked/unblocked', $this->notice[0]);
+ }
+
+ public function test_unblocking_accounts_clears_the_blocked_flag(): void {
+ // Pass two users to the module, one that is not blocked,
+ // and one that is.
+ $params = [
+ 'user' => [ 42, 44 ],
+ 'blocked' => [ false, true ],
+ 'page_accounts_block' => true,
+ ];
+
+ try {
+ $this->post('admin/accounts', [], $params);
+ } catch (RedirectException $redirect) {
+ $this->assertEquals(z_root() . '/admin/accounts', $redirect->getMessage());
+ }
+
+ // We expect the previously unblocked account to be blocked.
+ $account = get_account_by_id(42);
+ $this->assertEquals(ACCOUNT_BLOCKED, $account['account_flags'] & ACCOUNT_BLOCKED);
+
+ // We expect the previously blocked account to be unblocked.
+ $blocked_account = get_account_by_id(44);
+ $this->assertEquals(0, $blocked_account['account_flags'] & ACCOUNT_BLOCKED);
+
+ $this->assertEquals('2 account blocked/unblocked', $this->notice[0]);
+ }
+
+ public function test_deleting_accouns_remove_them_from_db(): void {
+ $params = [
+ 'user' => [ 42, 44 ],
+ 'page_accounts_delete' => true,
+ ];
+
+ try {
+ $this->post('admin/accounts', [], $params);
+ } catch (RedirectException $redirect) {
+ $this->assertEquals(z_root() . '/admin/accounts', $redirect->getMessage());
+ }
+
+ $this->assertEquals(null, get_account_by_id(42));
+ $this->assertEquals(null, get_account_by_id(44));
+ }
+
+ public function test_approving_pending_accounts_clears_pending_flag(): void {
+
+ // Catch calls to the php mail function
+ //
+ // This is just to get it out of the way, we don't care about
+ // how many times it's called, or with what args here.
+ $this->getFunctionMock('Zotlabs\Lib', 'mail')
+ ->expects($this->any())
+ ->willReturn(true);
+
+ $params = [
+ 'pending' => [
+ $this->fixtures['register'][0]['reg_hash'],
+ $this->fixtures['register'][1]['reg_hash']
+ ],
+ 'page_accounts_approve' => true,
+ ];
+
+ try {
+ $this->post('admin/accounts', [], $params);
+ } catch (RedirectException $redirect) {
+ $this->assertEquals(z_root() . '/admin/accounts', $redirect->getMessage());
+ }
+
+ foreach ([45, 46] as $id) {
+ $account = get_account_by_id($id);
+ $this->assertEquals(0, $account['account_flags'] & ACCOUNT_PENDING);
+ }
+ }
+
+ /**
+ * Stub the check_form_security_token_ForbiddenOnErr.
+ */
+ protected function stub_check_form_security(): void {
+ $this->stub_check_security =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'check_form_security_token_redirectOnErr')
+ ->expects($this->once())
+ ->with(
+ $this->identicalTo('/admin/accounts'),
+ $this->identicalTo('admin_accounts'))
+ ->willReturn(true);
+ }
+
+ /**
+ * Stub the call to is_site_admin in the Admin main module.
+ */
+ protected function stub_is_site_admin(): void {
+ $this->stub_is_site_admin =
+ $this->getFunctionMock('Zotlabs\Module', 'is_site_admin')
+ ->expects($this->once())
+ ->willReturn(true);
+ }
+
+ /**
+ * Stub the goaway function.
+ *
+ * Will throw an RedirectException with the URL being redirected to
+ * as the exception message.
+ *
+ * @throws RedirectException
+ */
+ protected function stub_goaway(): void {
+ $this->stub_goaway =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'goaway')
+ ->expects($this->once())
+ ->willReturnCallback(function (string $uri) {
+ throw new RedirectException($uri);
+ });
+ }
+
+ protected function stub_notice(): void {
+ $this->notice = [];
+ $this->stub_notice =
+ $this->getFunctionMock('Zotlabs\Module\Admin', 'notice')
+ ->expects($this->any())
+ ->willReturnCallback(function (string $arg) {
+ $this->notice[] = $arg;
+ });
+ }
+}
diff --git a/tests/unit/Module/HelpTest.php b/tests/unit/Module/HelpTest.php
new file mode 100644
index 000000000..1feef26a8
--- /dev/null
+++ b/tests/unit/Module/HelpTest.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ * SPDX-FileCopyrightText: 2024 Harald Eilertsen
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+class HelpTest extends \Zotlabs\Tests\Unit\Module\TestCase {
+
+ use \phpmock\phpunit\PHPMock;
+
+ /**
+ * Define the stubs to make sure they work later in the test.
+ *
+ * @see https://php-mock.github.io/php-mock-phpunit/api/class-phpmock.phpunit.PHPMock.html#_defineFunctionMock
+ *
+ * @beforeClass
+ */
+ public static function define_stubs(): void {
+ self::defineFunctionMock('Zotlabs\Lib\Traits', 'file_exists');
+ self::defineFunctionMock('Zotlabs\Module', 'file_get_contents');
+ }
+
+ /**
+ * Test getting a help page when the underlying file exists.
+ *
+ * @testWith
+ * ["md"]
+ * ["bb"]
+ * ["html"]
+ */
+ public function test_get_request_when_help_file_exists(string $ext): void {
+ $stubs = $this->prepare_stubs($ext);
+ $this->get("help/about/help_topic");
+
+ // Check that markdown content was correctly rendered
+ $this->assertPageContains('<h3>Help heading</h3>');
+
+ // Check that `$Projectname` has been translated properly
+ $this->assertPageContains('Hubzilla help content');
+
+ // Check that heading has been set
+ $this->assertPageContains('Hubzilla Documentation: About');
+
+ // Check that page title has been set
+ $this->assertTrue(isset(\App::$page['title']), 'Page title not set');
+ $this->assertStringContainsString('Help', \App::$page['title']);
+ $this->assertStringContainsStringIgnoringCase('Help Topic', \App::$page['title']);
+
+ // Check that nav selection has been set
+ $this->assertTrue(isset(\App::$nav_sel['raw_name']), 'Nav selection raw name not set');
+ $this->assertEquals('Help', \App::$nav_sel['raw_name']);
+
+ $this->assertTrue(isset(\App::$nav_sel['name']), 'Navselection name not set');
+ $this->assertEquals('Help', \App::$nav_sel['name']);
+ }
+
+ public function test_get_request_should_return_404_when_help_file_does_not_exist(): void {
+ // Stub file exists, to only retur true for the file with the current
+ // extension
+ $fe_stub = $this->getFunctionMock('Zotlabs\Lib\Traits', 'file_exists');
+ $fe_stub
+ ->expects($this->any())
+ ->willReturn(false);
+
+ // Make sure `file_get_contents` is never called by the code.
+ $fgc_stub = $this->getFunctionMock('Zotlabs\Module', 'file_get_contents');
+ $fgc_stub->expects($this->never());
+
+ $this->get("help/this_topic_does_not_exist");
+
+ $this->assertPageContains('not found');
+ }
+
+ public function test_get_request_without_args_redirects_to_about_page(): void {
+ $this->stub_goaway();
+ $this->expectException(\Zotlabs\Tests\Unit\Module\RedirectException::class);
+ $this->expectExceptionMessage('about/about');
+
+ $this->get('help');
+ }
+
+ public function test_getting_locale_with_no_topic_should_redirect_to_about_page(): void {
+ $this->expectRedirectTo('help/about/about');
+ $this->get('help/de');
+ }
+
+ public function test_find_help_file_returns_first_match(): void {
+ // Stub file exists, to always return true
+ $fe_stub = $this->getFunctionMock('Zotlabs\Lib\Traits', 'file_exists');
+ $fe_stub
+ ->expects($this->once())
+ ->willReturn(true);
+
+ // Stub `file_get_contents` to plant our own content.
+ $fgc_stub = $this->getFunctionMock('Zotlabs\Module', 'file_get_contents');
+ $fgc_stub
+ ->expects($this->once())
+ ->with('doc/en/first.md')
+ ->willReturn('found');
+
+ $this->get('help/first');
+ }
+
+ public function test_fall_back_to_english_if_localized_topic_dont_exist(): void {
+ \App::$language = 'nb';
+
+ $stubs = $this->prepare_stubs('bb');
+ $this->get('help/about/help_topic');
+
+ $this->assertPageContains('Hubzilla Documentation: About');
+ $this->assertPageContains('This page is not yet available in norsk bokmål');
+ }
+
+ public function test_includes(): void {
+ // Stub `file_get_contents` to plant our own content.
+ $fgc_stub = $this->getFunctionMock('Zotlabs\Module', 'file_get_contents');
+ $fgc_stub
+ ->expects($this->any())
+ ->willReturnCallback(
+ function (string $path): string {
+ if ($path === 'doc/en/sub.md') {
+ return "### This is the included file.";
+ } else {
+ return "### Main topic\n\n#include doc/en/sub.md;";
+ }
+ }
+ );
+
+ // Stub file exists, to always return true
+ $fe_stub = $this->getFunctionMock('Zotlabs\Lib\Traits', 'file_exists');
+ $fe_stub
+ ->expects($this->any())
+ ->willReturn(true);
+
+ $this->get('help/main');
+
+ $this->assertPageContains('<h3>This is the included file.</h3>');
+ }
+
+ public function test_include_file_of_different_type_than_main_file(): void {
+ // Stub `file_get_contents` to plant our own content.
+ $fgc_stub = $this->getFunctionMock('Zotlabs\Module', 'file_get_contents');
+ $fgc_stub
+ ->expects($this->any())
+ ->willReturnCallback(
+ function (string $path): string {
+ if ($path === 'doc/en/sub.md') {
+ return "### This is the included file.";
+ } else {
+ return "[h3]Main topic[/h3]\n\n#include doc/en/sub.md;";
+ }
+ }
+ );
+
+ // Stub file exists, only return true for main.bb and sub.md
+ $fe_stub = $this->getFunctionMock('Zotlabs\Lib\Traits', 'file_exists');
+ $fe_stub
+ ->expects($this->any())
+ ->willReturnCallback(
+ fn (string $path) => (
+ $path === 'doc/en/main.bb' || $path === 'doc/en/sub.md'
+ )
+ );
+
+ $this->get('help/main');
+
+ $this->assertPageContains('<h3>This is the included file.</h3>');
+ }
+
+ private function prepare_stubs(string $ext): array {
+ // Stub file exists, to only retur true for the file with the current
+ // extension
+ $fe_stub = $this->getFunctionMock('Zotlabs\Lib\Traits', 'file_exists');
+ $fe_stub
+ ->expects($this->any())
+ ->willReturnCallback(
+ fn (string $path) => $path === "doc/en/about/help_topic.{$ext}"
+ );
+
+ // Use a value map to make the `file_get_contents` stub return the
+ // correct content for the file types.
+ $file_content_map = [
+ [ 'doc/en/about/help_topic.md', "### Help heading\n\$Projectname help content" ],
+ [ 'doc/en/about/help_topic.bb', "[h3]Help heading[/h3]\n\n\$Projectname help content" ],
+ [ 'doc/en/about/help_topic.html', "<h3>Help heading</h3><p>\$Projectname help content</p>" ],
+ ];
+
+ // Stub `file_get_contents` to plant our own content.
+ $fgc_stub = $this->getFunctionMock('Zotlabs\Module', 'file_get_contents');
+ $fgc_stub
+ ->expects($this->once())
+ ->willReturnMap($file_content_map);
+
+ return [ 'file_exists' => $fe_stub, 'file_get_contents' => $fgc_stub ];
+ }
+}
diff --git a/tests/unit/Module/ItemTest.php b/tests/unit/Module/ItemTest.php
new file mode 100644
index 000000000..b461a3685
--- /dev/null
+++ b/tests/unit/Module/ItemTest.php
@@ -0,0 +1,56 @@
+<?php
+/*
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+use PHPUnit\Framework\Attributes\TestWith;
+
+class ItemTest extends TestCase {
+
+ #[TestWith(['application/x-zot+json'])]
+ #[TestWith(['application/x-zot-activity+json'])]
+ public function test_request_with_no_args_return_404(string $type): void {
+ $this->expect_status(404, 'Not found');
+
+ $_SERVER['HTTP_ACCEPT'] = $type;
+ $this->get('item');
+ }
+
+ #[TestWith(['application/x-zot+json'])]
+ #[TestWith(['application/x-zot-activity+json'])]
+ public function test_request_with_non_exiting_idem_id(string $type): void {
+ $this->expect_status(404, 'Not found');
+
+ $_SERVER['HTTP_ACCEPT'] = $type;
+ $this->get('item/non-existing-id');
+ }
+
+ /**
+ * Helper function to mock the `http_status_exit` function.
+ *
+ * The request will be terminated by throwing an exception, which
+ * will also terminate the test case. Iow. control will not return
+ * to the test case after the request has been made.
+ *
+ * @param int $status The expected HTTP status code.
+ * @param string $description The expected HTTP status description
+ */
+ private function expect_status(int $status, string $description): void {
+ $this->getFunctionMock('Zotlabs\Module', 'http_status_exit')
+ ->expects($this->once())
+ ->with($this->identicalTo($status), $this->identicalTo($description))
+ ->willReturnCallback(
+ function () {
+ throw new KillmeException();
+ }
+ );
+
+ $this->expectException(KillmeException::class);
+
+ }
+}
diff --git a/tests/unit/Module/MagicTest.php b/tests/unit/Module/MagicTest.php
new file mode 100644
index 000000000..4a03d9d57
--- /dev/null
+++ b/tests/unit/Module/MagicTest.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Tests for the Magic module
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+use PHPUnit\Framework\Attributes\BackupStaticProperties;
+use Zotlabs\Module\Magic;
+use App;
+
+class MagicTest extends TestCase {
+
+ public function test_init_with_no_args(): void {
+
+ // We expect the request to end with a status 400, as we do not
+ // pass any of the required params.
+ //
+ // To catch that, we have to mock the call to `http_status_exit`
+ // made by the code under test.
+ $this->getFunctionMock('Zotlabs\Module', 'http_status_exit')
+ ->expects($this->once()) // Expect the function to be called only once!
+ ->with( // ... with the following two arguments
+ $this->identicalTo(400),
+ $this->identicalTo('Bad Request')
+ )
+ ->willReturnCallback(function () { // Run this function instead when called
+ throw new KillmeException; // Throw an exception to terminate processing
+ });
+
+ // Tell the test system we excpect this exception to be thrown
+ $this->expectException(KillmeException::class);
+
+ $this->get('magic');
+ }
+
+ #[BackupStaticProperties(App::class)]
+ public function test_local_request_without_delegate(): void {
+ $baseurl = 'https://hubzilla.test';
+ $dest_url = $baseurl . '/channel/testuser';
+
+ App::set_baseurl($baseurl);
+
+ App::$observer = [
+ 'xchan_hash' => 'the hash',
+ ];
+
+ // We pass a local URL, and have a valid observer, but as the
+ // delegate param is not passed, nothing will be done except
+ // redirecting to the passed dest url.
+ //
+ // This should probably return a 400 Invalid Request instead.
+ $this->expectRedirectTo($dest_url);
+
+ $this->get('magic', [ 'bdest' => bin2hex($dest_url) ]);
+ }
+
+ #[BackupStaticProperties(App::class)]
+ public function test_delegate_request_switches_channel_when_allowed(): void {
+ $baseurl = 'https://hubzilla.test';
+ $dest_url = $baseurl . '/channel/testuser';
+
+ // Set the stage:
+ // Populate the global static App class with necessary values for the
+ // code under test
+ App::set_baseurl($baseurl);
+ App::$timezone = 'UTC';
+
+ // Simulate a foreign (to this hub) observer,
+ App::$observer = [
+ 'xchan_hash' => 'foreign hash',
+ ];
+
+ // Create the channel the foreign observer wants to access
+ $result = create_identity([
+ 'account_id' => $this->fixtures['account'][0]['account_id'],
+ 'nickname' => 'testuser',
+ 'name' => 'Trish Testuser',
+ ]);
+
+ // Shortcut the permission checks, by saying this observer is allowed
+ // the delegate privilege over the target channel
+ insert_hook('perm_is_allowed', function (array &$perm) {
+ $perm['result'] = true;
+ });
+
+ // Add some dummy session data, so we can check that it's being
+ // pushed to the delegate session.
+ $original_session = [
+ 'data' => 'Just some test session data',
+ ];
+
+ $_SESSION = $original_session;
+
+ // Handle redirects manually, since we want to be able to check some
+ // assertions after the redirect is thrown.
+ $this->stub_goaway();
+
+ try {
+ // Send a request to get delegate privileges for the `testuser` channel
+ // on the local hub.
+ $this->get('magic', [
+ 'bdest' => bin2hex($dest_url),
+ 'delegate' => 'testuser@hubzilla.test']
+ );
+ } catch (RedirectException $e) {
+ $this->assertEquals($dest_url, $e->getMessage());
+ $this->assertEquals($result['channel']['channel_id'], App::$channel['channel_id']);
+ $this->assertEquals($original_session, $_SESSION['delegate_push']);
+ $this->assertEquals($result['channel']['channel_id'], $_SESSION['delegate_channel']);
+ $this->assertEquals('foreign hash', $_SESSION['delegate']);
+ $this->assertEquals($this->fixtures['account'][0]['account_id'], $_SESSION['account_id']);
+ }
+ }
+}
diff --git a/tests/unit/Module/OwaTest.php b/tests/unit/Module/OwaTest.php
new file mode 100644
index 000000000..dbb25c0b5
--- /dev/null
+++ b/tests/unit/Module/OwaTest.php
@@ -0,0 +1,64 @@
+<?php
+/*
+ * SPDX-FileCopyrightText: 2025 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+class OwaTest extends TestCase
+{
+ public function testShouldReturnErrorIfNoAuthorizationHeader(): void
+ {
+ // Expect the call to return error
+ $this->expectJsonResponse([
+ 'success' => false,
+ 'message' => 'Missing or invalid authorization header.'
+ ]);
+
+ $this->get('owa');
+ }
+
+ public function testShouldReturnErrorIfWrongAuthorizationHeader(): void
+ {
+ // Expect the call to return error
+ $this->expectJsonResponse([
+ 'success' => false,
+ 'message' => 'Missing or invalid authorization header.'
+ ]);
+
+ $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer kjkjhkjhkjh';
+ $this->get('owa');
+ }
+
+ public function testShouldReturnErrorIfInvalidAuthorizationHeader(): void
+ {
+ // Expect the call to return error
+ $this->expectJsonResponse(['success' => false]);
+
+ $_SERVER['HTTP_AUTHORIZATION'] = 'Signature kjkjhkjhkjh';
+ $this->get('owa');
+ }
+
+ /**
+ * Expect the request to be terminated and return a json response.
+ */
+ private function expectJsonResponse(array $data): void
+ {
+ $this->getFunctionMock('Zotlabs\Module', 'json_return_and_die')
+ ->expects($this->once())
+ ->with(
+ $this->identicalTo($data),
+ $this->identicalTo('application/x-zot+json')
+ )
+ ->willReturnCallback(
+ function() {
+ throw new KillmeException();
+ }
+ );
+
+ $this->expectException(KillmeException::class);
+ }
+}
diff --git a/tests/unit/Module/RbmarkTest.php b/tests/unit/Module/RbmarkTest.php
new file mode 100644
index 000000000..594e7369b
--- /dev/null
+++ b/tests/unit/Module/RbmarkTest.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Unit/integration tests for the Rbmark module.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+class RbmarkTest extends \Zotlabs\Tests\Unit\Module\TestCase {
+ public function test_unauthenticated_get_request_return_login_form(): void {
+ $lc_stub = $this->getFunctionMock('Zotlabs\Module', 'local_channel');
+ $lc_stub
+ ->expects($this->once())
+ ->willReturn(false);
+
+ $this->get('rbmark', ['url' => 'https://bookmarked.url']);
+
+ $this->assertPageContains('value="login" />');
+
+ // also check that the original query is saved in the session
+ $this->assertEquals('https://bookmarked.url', $_SESSION['bookmark']['url']);
+ $this->assertEquals('rbmark', $_SESSION['bookmark']['q']);
+ }
+
+ public function test_authenticated_get_request_returns_save_bookmark_form(): void {
+ $lc_stub = $this->getFunctionMock('Zotlabs\Module', 'local_channel');
+ $lc_stub
+ ->expects($this->once())
+ ->willReturn(42);
+
+ $this->get('rbmark', [
+ 'url' => 'https://bookmarked.url',
+ 'title' => 'My bookmark',
+ ]);
+
+ $this->assertPageContains('<form action="rbmark" method="post"');
+ $this->assertPageContains('URL of bookmark');
+ $this->assertPageContains('value="https://bookmarked.url"');
+ $this->assertPageContains('value="My bookmark"');
+ }
+
+ public function test_that_params_are_escaped_in_save_bookmark_form(): void {
+ $lc_stub = $this->getFunctionMock('Zotlabs\Module', 'local_channel');
+ $lc_stub
+ ->expects($this->once())
+ ->willReturn(42);
+
+ $this->get('rbmark', [
+ 'url' => 'https://bookmarked.url" onload="alert(/boom/)',
+ 'title' => 'My bookmark"><script alert(/boom/);</script>',
+ ]);
+
+ $this->assertPageContains('value="https://bookmarked.url&quot; onload=&quot;alert(/boom/)');
+ $this->assertPageContains('value="My bookmark&quot;&gt;&lt;script alert(/boom/);&lt;/script&gt;');
+ }
+
+ public function test_that_existing_bookmark_folders_are_listed(): void {
+ $lc_stub = $this->getFunctionMock('Zotlabs\Module', 'local_channel');
+ $lc_stub
+ ->expects($this->once())
+ ->willReturn(42);
+
+ $menu_id = menu_create([
+ 'menu_name' => 'My bookmarks',
+ 'menu_desc' => 'A collection of my bookmarks',
+ 'menu_flags' => MENU_BOOKMARK,
+ 'menu_channel_id' => 42,
+ ]);
+
+ $this->get('rbmark', [
+ 'url' => 'https://bookmarked.url',
+ 'title' => 'My bookmark',
+ ]);
+
+ $this->assertPageContains(
+ "<option value=\"{$menu_id}\" >My bookmarks</option>"
+ );
+ }
+}
diff --git a/tests/unit/Module/RpostTest.php b/tests/unit/Module/RpostTest.php
new file mode 100644
index 000000000..ad94f2f06
--- /dev/null
+++ b/tests/unit/Module/RpostTest.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Tests for Rpost module.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+class RpostTest extends \Zotlabs\Tests\Unit\Module\TestCase {
+
+ /**
+ * Basic test of a get request with no args as an authenticated user.
+ */
+ public function test_get_with_no_args(): void {
+ $this->get_authenticated();
+
+ $this->assertPageContains('<form id="profile-jot-form"');
+ $this->assertPageContains('<input type="hidden" name="profile_uid" value="42"');
+ }
+
+ /**
+ * Display login form if session is not authenticated.
+ */
+ public function test_display_login_form_if_not_logged_in(): void {
+ $lc_mock = $this->getFunctionMock('Zotlabs\Module', 'local_channel')
+ ->expects($this->any())
+ ->willReturn(false);
+
+ $this->get('rpost');
+
+ $this->assertPageContains('<form action="https://hubzilla.test/rpost" id="main_login"');
+ }
+
+ public function test_populates_form_from_query_params(): void {
+ $this->get_authenticated([
+ 'title' => 'This is my title',
+ 'body' => 'The body of the post',
+ 'source' => 'The temple of the Dagon',
+ ]);
+
+ $this->assertPageContains('value="This is my title"');
+ $this->assertPageContains('>The body of the post</textarea>');
+ $this->assertPageContains('value="The temple of the Dagon"');
+ }
+
+ public function test_convert_body_from_html_to_bbcode(): void {
+ $this->get_authenticated([
+ 'body' => "<h1>Awesome page</h1>\r\n<p>Awesome content!</p>",
+ 'type' => 'html',
+ ]);
+
+ $this->assertPageContains(">[h1]Awesome page[/h1]\n\nAwesome content!</textarea>");
+ }
+
+ /**
+ * Private helper method to perform an authenticated GET request.
+ *
+ * @param array $query An associative array of query parameters.
+ */
+ private function get_authenticated(array $query = []): void {
+ // Mock `local_chanel()` to emulate a valid logged in channel
+ $lc_mock = $this->getFunctionMock('Zotlabs\Module', 'local_channel')
+ ->expects($this->any())
+ ->willReturn(42);
+
+ // Set basic access controls to keep AccessList happy.
+ \App::$channel = [
+ 'channel_id' => 42,
+ 'channel_location' => null,
+ 'channel_address' => '',
+ 'channel_allow_cid' => '',
+ 'channel_allow_gid' => '',
+ 'channel_deny_cid' => '',
+ 'channel_deny_gid' => '',
+ ];
+
+ $this->get('rpost', $query);
+ }
+}
diff --git a/tests/unit/Module/SetupTest.php b/tests/unit/Module/SetupTest.php
new file mode 100644
index 000000000..3575dd477
--- /dev/null
+++ b/tests/unit/Module/SetupTest.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * SPDX-FileCopyrightText: 2024 Harald Eilertsen
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+/**
+ * SetupModuleTest
+ *
+ * The Setup module should only be available during site installation. This is
+ * determined by whether there are any accounts present in the database, or
+ * not.
+ *
+ * This is a complex module, so expect the tests to grow as more of it will be
+ * covered.
+ */
+class SetupTest extends TestCase {
+
+ public function test_that_setup_is_available_if_no_accounts_in_db(): void {
+ $this->with_no_accounts_in_db();
+ $this->get('setup');
+
+ $this->assertEquals('setup', \App::$page['page_title']);
+
+ // Assert that result _don't_ match "Permission denied"
+ $this->assertThat(
+ \App::$page['content'],
+ $this->logicalNot(
+ $this->matchesRegularExpression('/Permission denied/')
+ )
+ );
+ }
+
+ public function test_that_setup_is_not_available_if_accounts_in_db(): void {
+ // The fixtures loaded by default add a couple of accounts.
+ $this->get('setup');
+
+ $this->assertEquals('setup', \App::$page['page_title']);
+ $this->assertMatchesRegularExpression('/Permission denied/', \App::$page['content']);
+ }
+
+ public function test_that_setup_testrewrite_returns_ok(): void {
+ // We need to stub the `killme` function, as it is called directly from
+ // the code under test.
+ $this->stub_killme();
+
+ $output = '';
+
+ // Turn on output buffering, as code under test echoes
+ // directly to the output
+ ob_start();
+ try {
+ $this->get('setup/testrewrite');
+ } catch (\Zotlabs\Tests\Unit\Module\KillmeException) {
+ $output = ob_get_contents();
+ }
+
+ $this->assertEquals('ok', $output);
+
+ ob_end_clean();
+ }
+
+ /**
+ * Delete all accounts from the database.
+ *
+ * This is currently needed because we automatically import the database
+ * fixtures on test start, which contains a couple of accounts already.
+ */
+ private function with_no_accounts_in_db(): void {
+ q('DELETE FROM account;');
+ }
+}
diff --git a/tests/unit/Module/TestCase.php b/tests/unit/Module/TestCase.php
new file mode 100644
index 000000000..1a4cf52fc
--- /dev/null
+++ b/tests/unit/Module/TestCase.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * This file contains the base class for module test cases.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Module;
+
+use PHPUnit\Framework\Attributes\After;
+use Zotlabs\Tests\Unit\UnitTestCase;
+use App;
+
+/**
+ * Base class for writing module tests.
+ *
+ * This test class adds a number of helper methods to the base from
+ * the UnitTestCase class, useful when testing Modules (Controllers)
+ * that handle incoming requests.
+ */
+class TestCase extends UnitTestCase {
+
+ // Import PHPMock methods into this class
+ use \phpmock\phpunit\PHPMock;
+
+ protected $killme_stub;
+ protected $goaway_stub;
+
+ #[After]
+ public function cleanup_stubs(): void {
+ $this->killme_stub = null;
+ $this->goaway_stub = null;
+ }
+
+ protected function do_request(string $method, string $uri, array $query = [], array $params = []): void {
+ $_GET['q'] = $uri;
+ $_GET = array_merge($_GET, $query);
+ $_POST = $params;
+
+ $_SERVER['REQUEST_METHOD'] = $method;
+ $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
+ $_SERVER['QUERY_STRING'] = "q={$uri}";
+ // phpcs:disable Generic.PHP.DisallowRequestSuperglobal.Found
+ $_REQUEST = array_merge($_GET, $_POST);
+ // phpcs::enable
+
+ \App::init();
+ \App::$page['content'] = '';
+
+ $router = new \Zotlabs\Web\Router();
+ $router->Dispatch();
+ }
+
+ /**
+ * Emulate a GET request.
+ *
+ * @param string $uri The URI to request. Typically this will be the module
+ * name, followed by any req args separated by slashes.
+ * @param array $query Assciative array of query args, with the parameters
+ * as keys.
+ */
+ protected function get(string $uri, array $query = []): void {
+ $this->do_request('GET', $uri, $query);
+ }
+
+ /**
+ * Emulate a POST request.
+ *
+ * @param string $uri The URI to request. Typically this will be the module
+ * name, followed by any req args separated by slashes.
+ * @param array $query Associative array of query args, with the parameters
+ * as keys.
+ * @param array $params Associative array of POST params, with the param names
+ * as keys.
+ */
+ protected function post(string $uri, array $query = [], array $params = []): void {
+ $this->do_request('POST', $uri, $query, $params);
+ }
+
+ /**
+ * Helper to simplify asserting contents in the rendered page.
+ *
+ * @param string $needle The expected string to find.
+ */
+ protected function assertPageContains(string $needle): void {
+ $this->assertStringContainsString($needle, App::$page['content']);
+ }
+
+ /**
+ * Stub out the `killme` function.
+ *
+ * Useful for testing modules that call this function directly.
+ *
+ * Instead of calling exit, the stub will throw a `KillmeException`,
+ * that can be caught by the test code to regain control after request
+ * processing is terminated.
+ *
+ * **Example:**
+ *
+ * public function test_something(): void {
+ * $this->stub_killme();
+ *
+ * try {
+ * killme();
+ * } catch (KillmeException $e) {
+ * $this->assertSomething(...);
+ * }
+ * }
+ *
+ * It's also possible to use the builting PHPUnit expecations to verify
+ * that the function was called.
+ *
+ * public function test_something(): void {
+ * $this->stub_killme();
+ * $this->expectException(KillmeException::class);
+ *
+ * killme();
+ * }
+ *
+ * This is useful if you only want to check that processing was terminated
+ * with the `killme()` function.
+ *
+ * @throws KillmeException
+ */
+ protected function stub_killme(): void {
+ $this->killme_stub = $this->getFunctionMock('Zotlabs\Module', 'killme')
+ ->expects($this->once())
+ ->willReturnCallback(
+ function () {
+ throw new KillmeException();
+ }
+ );
+ }
+
+ /**
+ * Stub out the `goaway` function.
+ *
+ * Useful for testing modules that calls this function directly.
+ *
+ * Instead of calling `killme()`, the stub will throw a RedirectException
+ * with the target URL as the exception message. This allows the test code
+ * to regain control after request processing is terminated.
+ *
+ * **Example:**
+ *
+ * public function test_redirect(): void {
+ * $this->stub_goaway();
+ *
+ * try {
+ * goaway('https://example.com/some_uri');
+ * } catch (RedirectException $e) {
+ * $this->assertEquals('https://example.com/some_uri', $e->getMessage());
+ * $this->assertSomethingElse(...);
+ * }
+ * }
+ * It's also possible to use the builting PHPUnit expecations to verify
+ * that the function was called.
+ *
+ * public function test_something(): void {
+ * $this->stub_goaway();
+ * $this->expectException(RedirectException::class);
+ * $this->expectExceptionMessage('https://example.com/some_uri');
+ *
+ * goaway('https://example.com/some_uri');
+ * }
+ *
+ * This is useful if you only want to check that the request was redirected.
+ *
+ * @throws RedirectException
+ */
+ protected function stub_goaway(): void {
+ $this->goaway_stub = $this->getFunctionMock('Zotlabs\Module', 'goaway')
+ ->expects($this->once())
+ ->willReturnCallback(
+ function (string $uri) {
+ throw new RedirectException($uri);
+ }
+ );
+ }
+
+ /**
+ * Shorthand function to expect a redirect to a given URL.
+ *
+ * **Example:**
+ *
+ * public function test_redirect(): void {
+ * $this->expectRedirectTo('https://example.com/some_uri');
+ * goaway('https://example.com/some_uri');
+ * }
+ */
+ protected function expectRedirectTo(string $destination): void {
+ $this->stub_goaway();
+ $this->expectException(RedirectException::class);
+ $this->expectExceptionMessage($destination);
+ }
+}
+
+/**
+ * Exception class for killme stub
+ */
+class KillmeException extends \Exception {}
+
+/**
+ * Exception class for goaway stub.
+ *
+ * Takes the goaway uri as an arg, and makes it available to the catch
+ * site via the `getMessage()` method.
+ */
+class RedirectException extends \Exception {
+ function __construct(string $uri) {
+ parent::__construct($uri);
+ }
+}