aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rwxr-xr-xtests/create_test_db.sh13
-rw-r--r--tests/phpunit.xml2
-rw-r--r--tests/unit/CleanupBBCodeTest.php27
-rw-r--r--tests/unit/Lib/ActivityStreamsTest.php136
-rw-r--r--tests/unit/Lib/ActivityTest.php245
-rw-r--r--tests/unit/Lib/JcsEddsa2022Test.php8
-rw-r--r--tests/unit/Lib/MailerTest.php62
-rw-r--r--tests/unit/Lib/MessageFilterTest.php207
-rw-r--r--tests/unit/Module/AdminAccountEditTest.php222
-rw-r--r--tests/unit/Module/AdminAccountsTest.php173
-rw-r--r--tests/unit/Module/HelpTest.php69
-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/TestCase.php64
-rw-r--r--tests/unit/Thumb/EpubthumbTest.php158
-rw-r--r--tests/unit/UnitTestCase.php10
-rw-r--r--tests/unit/Widget/HelpindexTest.php10
-rw-r--r--tests/unit/Widget/MessagesWidgetTest.php83
-rw-r--r--tests/unit/bootstrap.php9
-rw-r--r--tests/unit/includes/AccountTest.php21
-rw-r--r--tests/unit/includes/BBCodeTest.php51
-rw-r--r--tests/unit/includes/DatetimeTest.php53
-rw-r--r--tests/unit/includes/ItemsTest.php132
-rw-r--r--tests/unit/includes/LanguageTest.php8
-rw-r--r--tests/unit/includes/MarkdownTest.php35
-rw-r--r--tests/unit/includes/NetworkTest.php58
-rw-r--r--tests/unit/includes/PhotodriverTest.php58
-rw-r--r--tests/unit/includes/dba/DbaPdoTest.php140
-rw-r--r--tests/unit/includes/dba/_files/account.yml17
-rw-r--r--tests/unit/includes/dba/_files/register.yml20
31 files changed, 2261 insertions, 70 deletions
diff --git a/tests/create_test_db.sh b/tests/create_test_db.sh
index b98f5e2a5..dc9122aa0 100755
--- a/tests/create_test_db.sh
+++ b/tests/create_test_db.sh
@@ -40,6 +40,7 @@ case $HZ_TEST_DB_TYPE in
mariadb | mysql )
db_type="mysql"
+ db_binary=$HZ_TEST_DB_TYPE
default_charset="utf8mb4"
root_user="root"
root_passwd="root"
@@ -90,11 +91,11 @@ else
echo -e "\n--------------"
echo "Client version:"
echo -e "--------------\n"
- mysql --version
+ $db_binary --version
- mysql -v -u $HZ_TEST_DB_ROOT_USER -p$HZ_TEST_DB_ROOT_PASS -Ns -e "SELECT VERSION();"
+ $db_binary -v -u $HZ_TEST_DB_ROOT_USER -p$HZ_TEST_DB_ROOT_PASS -Ns -e "SELECT VERSION();"
- mysql -u $HZ_TEST_DB_ROOT_USER -p$HZ_TEST_DB_ROOT_PASS <<-EOSQL
+ $db_binary -u $HZ_TEST_DB_ROOT_USER -p$HZ_TEST_DB_ROOT_PASS <<-EOSQL
DROP DATABASE IF EXISTS $HZ_TEST_DB_NAME;
CREATE DATABASE $HZ_TEST_DB_NAME CHARACTER SET $HZ_TEST_DB_CHARSET;
@@ -107,7 +108,7 @@ else
echo -e "\n--------------"
echo "Importing schema..."
echo -e "--------------\n"
- mysql -u $HZ_TEST_DB_USER -p$HZ_TEST_DB_PASS $HZ_TEST_DB_NAME < ./install/schema_mysql.sql
- mysql -v -u $HZ_TEST_DB_ROOT_USER -p$HZ_TEST_DB_ROOT_PASS -Ns -e "show databases"
- mysql -v -u $HZ_TEST_DB_USER -p$HZ_TEST_DB_PASS $HZ_TEST_DB_NAME -Ns -e "show tables"
+ $db_binary -u $HZ_TEST_DB_USER -p$HZ_TEST_DB_PASS $HZ_TEST_DB_NAME < ./install/schema_mysql.sql
+ $db_binary -v -u $HZ_TEST_DB_ROOT_USER -p$HZ_TEST_DB_ROOT_PASS -Ns -e "show databases"
+ $db_binary -v -u $HZ_TEST_DB_USER -p$HZ_TEST_DB_PASS $HZ_TEST_DB_NAME -Ns -e "show tables"
fi
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 3531cd05c..44ee9c2ee 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="../boot.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache">
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="unit/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache">
<php>
<includePath>..</includePath>
<!-- env name="HZ_TEST_DB_HOST" value=""/-->
diff --git a/tests/unit/CleanupBBCodeTest.php b/tests/unit/CleanupBBCodeTest.php
new file mode 100644
index 000000000..8e19b1d7e
--- /dev/null
+++ b/tests/unit/CleanupBBCodeTest.php
@@ -0,0 +1,27 @@
+<?php
+/*
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+
+class CleanupBBCodeTest extends UnitTestCase {
+ #[DataProvider("cleanup_bbcode_provider")]
+ public function test_cleanup_bbcode(string $expected, string $input): void {
+ $this->assertEquals($expected, cleanup_bbcode($input));
+ }
+
+ public static function cleanup_bbcode_provider(): array {
+ return [
+ 'url followed by newline' => [
+ "#^[url=https://example.com]https://example.com[/url]\na test link",
+ "https://example.com\na test link",
+ ]
+ ];
+ }
+}
diff --git a/tests/unit/Lib/ActivityStreamsTest.php b/tests/unit/Lib/ActivityStreamsTest.php
new file mode 100644
index 000000000..38be1792e
--- /dev/null
+++ b/tests/unit/Lib/ActivityStreamsTest.php
@@ -0,0 +1,136 @@
+<?php
+/**
+ * Unit tests for Zotlabs\Lib\ActivityStreams.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Lib;
+
+use phpmock\phpunit\PHPMock;
+use Zotlabs\Lib\ActivityStreams;
+use Zotlabs\Tests\Unit\UnitTestCase;
+
+class ActivityStreamsTest extends UnitTestCase {
+
+ // Import PHPMock methods into this class
+ use PHPMock;
+
+ /**
+ * Test parsing an announce activity of a like from a remote server of
+ * a note from a third server.
+ *
+ * Also test that we fetch the referenced objects when the received
+ * activity is parsed,
+ */
+ public function test_parse_announce_of_like(): void {
+ $payload = <<<JSON
+ {
+ "actor": "https://lemmy.test/c/technology",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "object": {
+ "id": "https://lemmy.test/activities/like/e6e38c8b-beee-406f-9523-9da7ec97a823",
+ "actor": "https://lemmy.test/u/SomePerson",
+ "object": "https://somesite.test/post/1197552",
+ "type": "Like",
+ "audience": "https://lemmy.test/c/technology"
+ },
+ "cc": [
+ "https://lemmy.test/c/technology/followers"
+ ],
+ "type": "Announce",
+ "id": "https://lemmy.test/activities/announce/like/9e583a54-e4e0-4436-9726-975a14f923ed"
+ }
+ JSON;
+
+ //
+ // Mock z_fetch_url to prevent us from spamming real servers during test runs
+ //
+ // We just create some sample ActivityStreams objects to return for the various
+ // URL's to make it a somewhat realistic test. Each object will have it's URL as
+ // it's id and only specify the object type as laid out in the $urlmap below.
+
+ $urlmap = [
+ 'https://lemmy.test/c/technology' => [ 'type' => 'Group' ],
+ 'https://lemmy.test/u/SomePerson' => [ 'type' => 'Person' ],
+ 'https://somesite.test/post/1197552' => [ 'type' => 'Note' ],
+ ];
+
+ $z_fetch_url_stub = $this->getFunctionMock('Zotlabs\Lib', 'z_fetch_url');
+ $z_fetch_url_stub
+ ->expects($this->any())
+ ->willReturnCallback(function ($url) use ($urlmap) {
+ if (isset($urlmap[$url])) {
+ $body = json_encode(
+ array_merge([ 'id' => $url ], $urlmap[$url]),
+ JSON_FORCE_OBJECT,
+ );
+
+ return [
+ 'success' => true,
+ 'body' => $body,
+ ];
+ } else {
+ // We should perhaps throw an error here to fail the test,
+ // as we're receiving an unexpected URL.
+ return [
+ 'success' => false,
+ ];
+ }
+ });
+
+ // Make sure we have a sys channel before we start
+ create_sys_channel();
+
+ $as = new ActivityStreams($payload);
+
+ $this->assertTrue($as->valid);
+
+ $this->assertEquals(
+ 'https://lemmy.test/activities/announce/like/9e583a54-e4e0-4436-9726-975a14f923ed',
+ $as->id
+ );
+
+ $this->assertEquals('Announce', $as->type);
+
+ $this->assertIsArray($as->actor);
+ $this->assertArrayHasKey('id', $as->actor);
+ $this->assertEquals('https://lemmy.test/c/technology', $as->actor['id']);
+ $this->assertArrayHasKey('type', $as->actor);
+ $this->assertEquals('Group', $as->actor['type']);
+
+ $this->assertIsArray($as->recips);
+ $this->assertContains('https://www.w3.org/ns/activitystreams#Public', $as->recips);
+ $this->assertContains('https://lemmy.test/c/technology/followers', $as->recips);
+ $this->assertContains('https://lemmy.test/c/technology', $as->recips);
+
+ $this->assertIsArray($as->obj);
+ $this->assertArrayHasKey('id', $as->obj);
+ $this->assertEquals(
+ 'https://lemmy.test/activities/like/e6e38c8b-beee-406f-9523-9da7ec97a823',
+ $as->obj['id']
+ );
+ $this->assertArrayHasKey('type', $as->obj);
+ $this->assertEquals('Like', $as->obj['type']);
+ $this->assertArrayHasKey('object', $as->obj);
+
+ $this->assertIsArray($as->obj['object']);
+
+ $this->assertArrayHasKey('id', $as->obj['object']);
+ $this->assertEquals('https://somesite.test/post/1197552', $as->obj['object']['id']);
+
+ $this->assertArrayHasKey('type', $as->obj['object']);
+ $this->assertEquals('Note', $as->obj['object']['type']);
+
+ $this->assertIsArray($as->obj['actor']);
+ $this->assertArrayHasKey('id', $as->obj['actor']);
+ $this->assertEquals('https://lemmy.test/u/SomePerson', $as->obj['actor']['id']);
+ $this->assertArrayHasKey('type', $as->obj['actor']);
+ $this->assertEquals('Person', $as->obj['actor']['type']);
+ }
+}
diff --git a/tests/unit/Lib/ActivityTest.php b/tests/unit/Lib/ActivityTest.php
index 0e2703f2b..46f53ecd9 100644
--- a/tests/unit/Lib/ActivityTest.php
+++ b/tests/unit/Lib/ActivityTest.php
@@ -5,8 +5,13 @@ error_reporting(E_ALL);
use Zotlabs\Tests\Unit\UnitTestCase;
use Zotlabs\Lib\Activity;
+use Zotlabs\Lib\ActivityStreams;
+use phpmock\phpunit\PHPMock;
class ActivityTest extends UnitTestCase {
+ // Import PHPMock methods into this class
+ use PHPMock;
+
/**
* Test get a textfield from an activitystreams object
*
@@ -36,4 +41,244 @@ class ActivityTest extends UnitTestCase {
];
}
+
+ /**
+ *
+ * @dataProvider get_mid_and_uuid_provider
+ */
+ public function test_get_mid_and_uuid(string $payload, string $mid, string $uuid): void {
+
+
+ //
+ // Mock z_fetch_url to prevent us from spamming real servers during test runs
+ //
+ // We just create some sample ActivityStreams objects to return for the various
+ // URL's to make it a somewhat realistic test. Each object will have it's URL as
+ // it's id and only specify the object type as laid out in the $urlmap below.
+
+ $item_json = '{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://purl.archive.org/socialweb/webfinger",{"zot":"https://hub.somaton.com/apschema#","contextHistory":"https://w3id.org/fep/171b/contextHistory","schema":"http://schema.org#","ostatus":"http://ostatus.org#","diaspora":"https://diasporafoundation.org/ns/","litepub":"http://litepub.social/ns#","toot":"http://joinmastodon.org/ns#","commentPolicy":"zot:commentPolicy","Bookmark":"zot:Bookmark","Category":"zot:Category","Emoji":"toot:Emoji","directMessage":"litepub:directMessage","PropertyValue":"schema:PropertyValue","value":"schema:value","uuid":"schema:identifier","conversation":"ostatus:conversation","guid":"diaspora:guid","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","Hashtag":"as:Hashtag"}],"type":"Note","id":"https://hub.somaton.com/item/7bb5da01-f97b-408f-853a-eb4d09079e5a","diaspora:guid":"7bb5da01-f97b-408f-853a-eb4d09079e5a","published":"2025-04-14T05:43:00Z","attributedTo":"https://hub.somaton.com/channel/mario","inReplyTo":"https://social.wake.st/users/liaizon/statuses/114332208953644534","context":"https://social.wake.st/users/liaizon/statuses/114332208953644534","content":"@<a href=\"https://social.wake.st/users/liaizon\" target=\"_blank\" rel=\"nofollow noopener\" >wakest \u2042</a>,<br />@<a class=\"zrl\" href=\"https://hubzilla.org/channel/info\" target=\"_blank\" rel=\"nofollow noopener\" >Hubzilla</a> has had <span style=\"color: magenta;\">colors</span> <span style=\"color: lightgreen;\">since</span> <span style=\"color: blue;\">10+</span> <span style=\"color: orange;\">years</span> :grinning_face_with_smiling_eyes:","source":{"content":"@[url=https://social.wake.st/users/liaizon]wakest \u2042[/url],\r\n@[zrl=https://hubzilla.org/channel/info]Hubzilla[/zrl] has had [color=magenta]colors[/color] [color=lightgreen]since[/color] [color=blue]10+[/color] [color=orange]years[/color] :grinning_face_with_smiling_eyes:","mediaType":"text/bbcode"},"actor":"https://hub.somaton.com/channel/mario","tag":[{"type":"Mention","href":"https://social.wake.st/users/liaizon","name":"@wakest \u2042"},{"type":"Mention","href":"https://hubzilla.org/channel/info","name":"@Hubzilla"},{"type":"Emoji","id":"https://hub.somaton.com/emoji/grinning_face_with_smiling_eyes","name":":grinning_face_with_smiling_eyes:","icon":{"type":"Image","url":"https://hub.somaton.com/addon/emoji/emojitwo/1f604.png"}}],"to":["https://www.w3.org/ns/activitystreams#Public","https://social.wake.st/users/liaizon","https://hubzilla.org/channel/info"],"cc":["https://social.wake.st/users/liaizon/followers","https://app.wafrn.net/fediverse/blog/wafrn"],"proof":{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2025-04-14T15:18:46Z","verificationMethod":"https://hub.somaton.com/channel/mario#z6MkfHv7DiVBDs7qZJVfbLUHLbKFYBxdhDBeqHRmhpWq8Pj9","proofPurpose":"assertionMethod","proofValue":"z5bxnZPhccvxtKZAMPxPexeqHgfzmL6U9jX6mSDbUCi3xmtLgsDbjMtAvMS2f8qw9rHBsFHyo2999xWmfPDZsCZ8U"},"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"2fd1920c5b8906ce4d3bfbdd9ff967c10e5e37202e227fb40dc0a2a14e887093","creator":"https://hub.somaton.com/channel/mario","created":"2025-04-14T15:18:46Z","signatureValue":"Xlg0UISgRoeTfB2Qpy/UqZ4jQeNeGUVx0Gztp7uhqtj8lbQNmWyZqUzXdXf7jGjiVcy87rmdakSfcQG9Zvbak/illLePj8pkXixLWdquoyJ5v/MhDtfgEoKikGSP3mkunabNNL1yFm5uZ6dYjS4ea0lB/1YPIyWjP7NhLbv0+HD/02a9P87Nlwufh1PUFoL9Y4XPIJpparz5Wax5fIfqzmVSMa0QLN+d/zQb+/jdOszhdiTZdUgpRK4Yb8xKeRBO1kOngtOfD0I7szUdRlTmFIpi83HKRNTAJrGyFsCwTnZmIy0dHhxmyvarHyz2kuEcf8nz3z5BV8amo7edAx9wWizsRiYaiMQ65mgl6wfZapHzkUqGH7mT9Kp3YmTOTgCy9OyP7yXyUUx5MRnyqQnjzoEw6stQwNb+IuhfwRcoLwJ/sIY5db29FK3QrbMCKNvxxJUjBqq+rdUjXnpvpdm9i8X1oJ1dHtkqSNoOBleykNudxyDRjvy+uI9z6OLt3LyNorOQ+3RUxxSxONoAsb+DVuLldMfD8ASVZWMzPr2CnyAuf8EFHccCoHibiNbMRuovk+kcLd+47B+v/tOq+rk6bPQ+np323nyUYZDGrH7KYgkQuXA83E2bLd3pOFfVQjDGEJlwrSx3U7wj+DDQohN1DqIkoK7WBpU1cFI6nn0r6ak="}}';
+
+ $urlmap = [
+ 'https://hub.somaton.com/channel/mario' => [ 'type' => 'Person' ],
+ 'https://mitra.social/users/silverpill' => [ 'type' => 'Person' ],
+ 'https://hub.somaton.com/item/7bb5da01-f97b-408f-853a-eb4d09079e5a' => json_decode($item_json, true)
+ ];
+
+ $z_fetch_url_stub = $this->getFunctionMock('Zotlabs\Lib', 'z_fetch_url');
+ $z_fetch_url_stub
+ ->expects($this->any())
+ ->willReturnCallback(function ($url) use ($urlmap) {
+ if (isset($urlmap[$url])) {
+ $body = json_encode(
+ array_merge([ 'id' => $url ], $urlmap[$url]),
+ JSON_FORCE_OBJECT,
+ );
+
+ return [
+ 'success' => true,
+ 'body' => $body,
+ ];
+ } else {
+ // We should perhaps throw an error here to fail the test,
+ // as we're receiving an unexpected URL.
+ return [
+ 'success' => false,
+ ];
+ }
+ });
+
+ // Make sure we have a sys channel before we start
+ create_sys_channel();
+
+ $as = new ActivityStreams($payload);
+
+ $this->assertEquals($mid, Activity::getMessageID($as));
+ $this->assertEquals($uuid, Activity::getUUID($as));
+ }
+
+ /**
+ * Dataprovider for test_get_mid_and_uuid.
+ */
+ public static function get_mid_and_uuid_provider(): array {
+ return [
+ 'Note from hubzilla with diaspora:guid' => [
+ '{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ "https://purl.archive.org/socialweb/webfinger",
+ {
+ "zot":"https://hub.somaton.com/apschema#",
+ "contextHistory":"https://w3id.org/fep/171b/contextHistory",
+ "schema":"http://schema.org#",
+ "ostatus":"http://ostatus.org#",
+ "diaspora":"https://diasporafoundation.org/ns/",
+ "litepub":"http://litepub.social/ns#",
+ "toot":"http://joinmastodon.org/ns#",
+ "commentPolicy":"zot:commentPolicy",
+ "Bookmark":"zot:Bookmark",
+ "Category":"zot:Category",
+ "Emoji":"toot:Emoji",
+ "directMessage":"litepub:directMessage",
+ "PropertyValue":"schema:PropertyValue",
+ "value":"schema:value",
+ "uuid":"schema:identifier",
+ "conversation":"ostatus:conversation",
+ "guid":"diaspora:guid",
+ "manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
+ "Hashtag":"as:Hashtag"
+ }
+ ],
+ "type":"Note",
+ "id":"https://hub.somaton.com/item/2e4b2cfa-7c20-49c2-b192-ae54f43a211a",
+ "diaspora:guid":"2e4b2cfa-7c20-49c2-b192-ae54f43a211a",
+ "published":"2025-04-03T17:45:41Z",
+ "commentPolicy":"contacts",
+ "attributedTo":"https://hub.somaton.com/channel/mario",
+ "contextHistory":"https://hub.somaton.com/conversation/2e4b2cfa-7c20-49c2-b192-ae54f43a211a",
+ "context":"https://hub.somaton.com/conversation/2e4b2cfa-7c20-49c2-b192-ae54f43a211a",
+ "content":"Looks like we have a :hubzilla: emoji now :slightly_smiling_face:",
+ "source":{
+ "content":"Looks like we have a :hubzilla: emoji now :slightly_smiling_face:",
+ "mediaType":"text/bbcode"
+ },
+ "actor":"https://hub.somaton.com/channel/mario",
+ "tag":[
+ {
+ "type":"Emoji",
+ "id":"https://hub.somaton.com/emoji/hubzilla",
+ "name":":hubzilla:",
+ "icon":{
+ "type":"Image",
+ "url":"https://hub.somaton.com/images/hubzilla.svg"
+ }
+ },
+ {
+ "type":"Emoji",
+ "id":"https://hub.somaton.com/emoji/slightly_smiling_face",
+ "name":":slightly_smiling_face:",
+ "icon":{
+ "type":"Image",
+ "url":"https://hub.somaton.com/images/emoji/slightly_smiling_face.png"
+ }
+ }
+ ],
+ "to":[
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc":[
+ "https://hub.somaton.com/followers/mario"
+ ],
+ "proof":{
+ "type":"DataIntegrityProof",
+ "cryptosuite":"eddsa-jcs-2022",
+ "created":"2025-04-14T15:25:17Z",
+ "verificationMethod":"https://hub.somaton.com/channel/mario#z6MkfHv7DiVBDs7qZJVfbLUHLbKFYBxdhDBeqHRmhpWq8Pj9",
+ "proofPurpose":"assertionMethod",
+ "proofValue":"zJf7xXBtD6ZTG27171X7X1BSkw7kijw4MCvzhowL7giB5s3mUKbo9yF1wq29E3bvHc3Q7HbDzUdbFE8cpCYYH9uJ"
+ },
+ "signature":{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1"
+ ],
+ "type":"RsaSignature2017",
+ "nonce":"3cda8c90fdbe708d625a017d1c946c8144fa288c9a04124b40b27f2f6a429e94",
+ "creator":"https://hub.somaton.com/channel/mario",
+ "created":"2025-04-14T15:25:17Z",
+ "signatureValue":"d01N0pMca7I9/dCdYbwuY3/SUe0xCZfwRSPxA7w9Pj4fFYDwhCNVYLKWz66K7RP7KfDD7DQ3oS8wLxn4qSX7wjFDJhwy7PkbGUzawBc9eti+8wHbiMGD2JgZCbzGaXmR/k5zyOykKhqglUOr0BcvAfqM1g3+7UxtYdxMNFlJ9nAJTObmd5jR8RyPe9b7Tbgi5XJDJ4U4qLsb8tAK54Sr2208fuCs7T+baErMPNj4eVprWoJObnr6sQX4YJH3404eJpExMLSu+y9taWLXxg6qDv+EY/RjgbKh/cdYNB5ERDFVK4WxgrrJCTv5t7mdVxjDN3sHUsfeT1aF2JYbS5ISSdtdHZnEMNIw3uwXLm5zG76fk17nUdDfXm1Pyu2uAuwRYIBOMQWeFZvdvo1Lf457kCQQN0DgUv3t89JD7Y5fZAzOlKiqXb52cxlsNUQFw8vQnWLGZNdqpDU0np6IhABrsMo+QoYrQepwKzxnmy8cwB6KKyD8W75H49l79DslDvg71nue3MuLtIfaI+d1GhYIul9o0ttJnzTbvg6L+pLLtzwgsDCqVhkXgQmk7J8RUuux9gmqYMe0pCoDlrVcR0Jhte57JqgqYZck3BPupLuu+Pg4n5/SIAogsCCWOu4ygV7jwAOcPmze7XozBuP7CVFVpgfooo9rU3kPKeEETkcljKg="
+ }
+ }',
+ 'https://hub.somaton.com/item/2e4b2cfa-7c20-49c2-b192-ae54f43a211a',
+ '2e4b2cfa-7c20-49c2-b192-ae54f43a211a'
+ ],
+
+ 'Like(Note) from mitra without uuid' => [
+ '{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ "https://w3id.org/security/data-integrity/v1",
+ {
+ "Emoji":"toot:Emoji",
+ "EmojiReact":"litepub:EmojiReact",
+ "Hashtag":"as:Hashtag",
+ "litepub":"http://litepub.social/ns#",
+ "sensitive":"as:sensitive",
+ "toot":"http://joinmastodon.org/ns#"
+ }
+ ],
+ "actor":"https://mitra.social/users/silverpill",
+ "cc":[
+
+ ],
+ "id":"https://mitra.social/activities/like/01963430-e998-4bef-c903-50903dde06dc",
+ "object":"https://hub.somaton.com/item/7bb5da01-f97b-408f-853a-eb4d09079e5a",
+ "proof":{
+ "created":"2025-04-14T12:05:42.945286724Z",
+ "cryptosuite":"eddsa-jcs-2022",
+ "proofPurpose":"assertionMethod",
+ "proofValue":"z4XrZRkUBoxmAn1xYXwmhaJTXb9Mog9C3cjrPenRhqWQbfXv6QJmMyGydsQ3LqN61uVfRvis8RoTymyUPqy76k9mg",
+ "type":"DataIntegrityProof",
+ "verificationMethod":"https://mitra.social/users/silverpill#ed25519-key"
+ },
+ "to":[
+ "https://hub.somaton.com/channel/mario",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type":"Like"
+ }',
+ 'https://mitra.social/activities/like/01963430-e998-4bef-c903-50903dde06dc',
+ ''
+ ],
+
+ 'Like(Note) from mitra with manually added uuid' => [
+ '{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ "https://w3id.org/security/data-integrity/v1",
+ {
+ "Emoji":"toot:Emoji",
+ "EmojiReact":"litepub:EmojiReact",
+ "Hashtag":"as:Hashtag",
+ "litepub":"http://litepub.social/ns#",
+ "sensitive":"as:sensitive",
+ "toot":"http://joinmastodon.org/ns#"
+ }
+ ],
+ "actor":"https://mitra.social/users/silverpill",
+ "cc":[
+
+ ],
+ "id":"https://mitra.social/activities/like/01963430-e998-4bef-c903-50903dde06dc",
+ "uuid":"01963430-e998-4bef-c903-50903dde06dc",
+ "object":"https://hub.somaton.com/item/7bb5da01-f97b-408f-853a-eb4d09079e5a",
+ "proof":{
+ "created":"2025-04-14T12:05:42.945286724Z",
+ "cryptosuite":"eddsa-jcs-2022",
+ "proofPurpose":"assertionMethod",
+ "proofValue":"z4XrZRkUBoxmAn1xYXwmhaJTXb9Mog9C3cjrPenRhqWQbfXv6QJmMyGydsQ3LqN61uVfRvis8RoTymyUPqy76k9mg",
+ "type":"DataIntegrityProof",
+ "verificationMethod":"https://mitra.social/users/silverpill#ed25519-key"
+ },
+ "to":[
+ "https://hub.somaton.com/channel/mario",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type":"Like"
+ }',
+ 'https://mitra.social/activities/like/01963430-e998-4bef-c903-50903dde06dc',
+ '01963430-e998-4bef-c903-50903dde06dc'
+ ]
+ ];
+ }
+
+ public function testBuildPacketWithEmptyChannel(): void {
+ $data = [ 'aKey' => 'aValue' ];
+ $packet = json_decode(Activity::build_packet($data, []), true);
+
+ $this->assertArrayHasKey('aKey', $packet);
+ $this->assertEquals('aValue', $packet['aKey']);
+ }
}
diff --git a/tests/unit/Lib/JcsEddsa2022Test.php b/tests/unit/Lib/JcsEddsa2022Test.php
index d18ad01ce..7cdc655f8 100644
--- a/tests/unit/Lib/JcsEddsa2022Test.php
+++ b/tests/unit/Lib/JcsEddsa2022Test.php
@@ -3,6 +3,7 @@
namespace Zotlabs\Tests\Unit\Lib;
use Zotlabs\Lib\JcsEddsa2022;
+use Zotlabs\Lib\JcsEddsa2022SignException;
use Zotlabs\Tests\Unit\UnitTestCase;
class JcsEddsa2022Test extends UnitTestCase {
@@ -171,4 +172,11 @@ class JcsEddsa2022Test extends UnitTestCase {
$this->assertTrue($verified, 'Verify encode and decode eddsa-jcs-2022');
}
+
+ public function testSignWithInvalidChannelShouldBeRejected(): void {
+ $this->expectException(JcsEddsa2022SignException::class);
+
+ $alg = new JcsEddsa2022();
+ $res = $alg->sign([], []);
+ }
}
diff --git a/tests/unit/Lib/MailerTest.php b/tests/unit/Lib/MailerTest.php
new file mode 100644
index 000000000..038c7ef4c
--- /dev/null
+++ b/tests/unit/Lib/MailerTest.php
@@ -0,0 +1,62 @@
+<?php
+/*
+ * Tests for the Zotlabs\LibM̀ailer class.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Lib;
+
+use App;
+use phpmock\phpunit\PHPMock;
+use Zotlabs\Lib\Mailer;
+use Zotlabs\Tests\Unit\UnitTestCase;
+
+class MailerTest extends UnitTestCase {
+
+ use PHPMock;
+
+ public function test_optional_params_replaced_by_defaults(): void {
+ $hostname = App::get_hostname();
+ $recipient = 'recipient@somesite.test';
+ $subject = 'A test email';
+ $body = <<<EOF
+ Dear recipient,
+
+ This is an test email message for you.
+
+ Sincerely,
+ Hubzilla
+ EOF;
+
+ //
+ // Catch calls to the php mail function, and verify
+ // that it is called with the args we're expecting
+ //
+ $this->getFunctionMock('Zotlabs\Lib', 'mail')
+ ->expects($this->once())
+ ->with(
+ $this->identicalTo($recipient),
+ $this->identicalTo($subject),
+ $this->identicalTo($body),
+ $this->identicalTo(<<<EOF
+ From: <Administrator@{$hostname}>
+ Reply-To: <noreply@{$hostname}>
+ Content-Type: text/plain; charset=UTF-8
+ EOF
+ )
+ )
+ ->willReturn(true);
+
+ $mailer = new Mailer([
+ 'toEmail' => $recipient,
+ 'messageSubject' => $subject,
+ 'textVersion' => $body,
+ ]);
+
+ $mailer->deliver();
+ }
+}
diff --git a/tests/unit/Lib/MessageFilterTest.php b/tests/unit/Lib/MessageFilterTest.php
new file mode 100644
index 000000000..0a2aea0c6
--- /dev/null
+++ b/tests/unit/Lib/MessageFilterTest.php
@@ -0,0 +1,207 @@
+<?php
+namespace Zotlabs\Tests\Unit\Lib;
+
+use Zotlabs\Tests\Unit\UnitTestCase;
+use Zotlabs\Lib\MessageFilter;
+
+class MessageFilterTest extends UnitTestCase {
+
+ /**
+ * Test the `evaluate` function.
+ *
+ * @dataProvider evaluate_provider
+ */
+ public function test_evaluate(string $incl, string $excl, bool $result) : void {
+ // This is for simpler handling of the timestamps
+ date_default_timezone_set('UTC');
+
+ $item = [
+ 'title' => '',
+ 'body' => "A grasshopper spent the summer hopping about in the sun and singing to his heart's content. One day, an ant went hurrying by, looking very hot and weary.\r\n#story #grasshopper #ant",
+ 'term' => [
+ ['ttype' => TERM_HASHTAG, 'term' => 'story'],
+ ['ttype' => TERM_HASHTAG, 'term' => 'grasshopper'],
+ ['ttype' => TERM_HASHTAG, 'term' => 'ant']
+ ],
+ 'verb' => 'Create',
+ 'obj_type' => 'Note',
+ 'obj' => [
+ 'type' => 'Note',
+ 'attributedTo' => 'https://example.com/users/test',
+ 'summary' => null,
+ 'content' => "A grasshopper spent the summer hopping about in the sun and singing to his heart's content. One day, an ant went hurrying by, looking very hot and weary.\r\n#story #grasshopper #ant",
+ 'sensitive' => false
+ ],
+ 'item_private' => 0,
+ 'item_thread_top' => 1,
+ 'created' => '2025-04-18 20:50:00'
+ ];
+
+ $this->assertEquals($result, MessageFilter::evaluate($item, $incl, $excl));
+ }
+
+ public static function evaluate_provider() : array {
+ return [
+ 'body contains incl' => [
+ 'summer',
+ '',
+ true
+ ],
+ 'body contains excl' => [
+ '',
+ 'summer',
+ false
+ ],
+ 'body contains word hopper (starting with a space) in excl using regex' => [
+ '',
+ '/ hopper/',
+ true
+ ],
+ 'lang=en in incl' => [
+ 'lang=en',
+ '',
+ true
+ ],
+ 'lang=en in excl' => [
+ '',
+ 'lang=en',
+ false
+ ],
+ 'lang=de in incl' => [
+ 'lang=de',
+ '',
+ false
+ ],
+ 'lang=de in excl' => [
+ '',
+ 'lang=de',
+ true
+ ],
+ 'until=2025-04-18 20:49:00 in excl' => [
+ '',
+ 'until=2025-04-18 20:49:00',
+ true
+ ],
+ 'until=2025-04-18 20:51:00 in excl' => [
+ '',
+ 'until=2025-04-18 20:51:00',
+ false
+ ],
+ 'until=2025-04-18 20:49:00 in incl' => [
+ 'until=2025-04-18 20:49:00',
+ '',
+ false
+ ],
+ 'until=2025-04-18 20:51:00 in incl' => [
+ 'until=2025-04-18 20:51:00',
+ '',
+ true
+ ],
+ 'hashtag in incl' => [
+ '#grasshopper',
+ '',
+ true
+ ],
+ 'hashtag in excl' => [
+ '',
+ '#grasshopper',
+ false
+ ],
+ 'any hashtag in excl' => [
+ '',
+ '#*',
+ false
+ ],
+ 'item.body contains substring hopper in excl' => [
+ '',
+ '?body ~= hopper',
+ false
+ ],
+ 'item.verb == Announce in excl' => [
+ '',
+ '?verb == Announce',
+ true
+ ],
+ 'item.verb != Announce in incl' => [
+ '?verb != Announce',
+ '',
+ true
+ ],
+ 'combined body contains word and item.verb == Announce in excl' => [
+ '',
+ "summer\r\n?verb == Announce",
+ false
+ ],
+ 'item.item_thread_top == 1 in excl' => [
+ '',
+ "?item_thread_top == 1",
+ false
+ ],
+ 'combined item_private == 0 and item.item_thread_top == 1 in excl' => [
+ '',
+ "?item_private == 0\r\n?item_thread_top == 1",
+ false
+ ],
+ 'item.item_private < 1 in excl' => [
+ '',
+ "?item_private < 1",
+ false
+ ],
+ 'item.item_thread_top = 1 and item.item_private > 0 in excl' => [
+ '',
+ "?item_thread_top == 1 && ?item_private > 0 ",
+ true
+ ],
+ 'item.item_thread_top = 1 and item.item_private < 1 in excl' => [
+ '',
+ "?item_thread_top == 1 && ?item_private < 1 ",
+ false
+ ],
+ 'item.item_thread_top = 1 or item.item_private = 0 in excl' => [
+ '',
+ "?item_thread_top == 1 && ?item_private == 0",
+ false
+ ],
+ 'item.item_private < 1 and item.item_thread_top = 1 in excl' => [
+ '',
+ "?item_private < 1 && ?item_thread_top == 1",
+ false
+ ],
+ 'item.item_private < 1 and item.item_thread_top = 0 in excl' => [
+ '',
+ "?item_private < 1 && ?item_thread_top == 0",
+ true
+ ],
+ 'combined item.verb = Create, item.item_private < 1 and item.item_thread_top = 0 in excl' => [
+ '',
+ "?verb == Create\r\n?item_private < 1 && ?item_thread_top == 1",
+ false
+ ],
+ 'item.obj contains value Note in incl' => [
+ '?obj {} Note',
+ '',
+ true
+ ],
+ 'item.obj contains key type in incl' => [
+ '?obj {*} type',
+ '',
+ true
+ ],
+ 'obj.type = Note in incl' => [
+ '?+type == Note',
+ '',
+ true
+ ],
+ 'obj.sensitive = true in incl' => [
+ '?+sensitive',
+ '',
+ false
+ ],
+ 'obj.sensitive != false in incl' => [
+ '?+!sensitive',
+ '',
+ true
+ ],
+ ];
+ }
+}
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
index c345d5e52..e1aea1a06 100644
--- a/tests/unit/Module/HelpTest.php
+++ b/tests/unit/Module/HelpTest.php
@@ -31,30 +31,7 @@ class HelpTest extends \Zotlabs\Tests\Unit\Module\TestCase {
* ["html"]
*/
public function test_get_request_when_help_file_exists(string $ext): 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())
- ->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);
-
-
+ $stubs = $this->prepare_stubs($ext);
$this->get("help/about/help_topic");
// Check that markdown content was correctly rendered
@@ -99,11 +76,16 @@ class HelpTest extends \Zotlabs\Tests\Unit\Module\TestCase {
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->expectExceptionMessage('about');
$this->get('help');
}
+ public function test_getting_locale_with_no_topic_should_redirect_to_about_page(): void {
+ $this->expectRedirectTo('help/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');
@@ -121,6 +103,16 @@ class HelpTest extends \Zotlabs\Tests\Unit\Module\TestCase {
$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');
@@ -176,4 +168,31 @@ class HelpTest extends \Zotlabs\Tests\Unit\Module\TestCase {
$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..2c426bf76
--- /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::set_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::set_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/TestCase.php b/tests/unit/Module/TestCase.php
index e92bc7083..dd88a5a3b 100644
--- a/tests/unit/Module/TestCase.php
+++ b/tests/unit/Module/TestCase.php
@@ -10,6 +10,7 @@
namespace Zotlabs\Tests\Unit\Module;
+use PHPUnit\Framework\Attributes\After;
use Zotlabs\Tests\Unit\UnitTestCase;
use App;
@@ -25,26 +26,27 @@ class TestCase extends UnitTestCase {
// Import PHPMock methods into this class
use \phpmock\phpunit\PHPMock;
- /**
- * 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 {
- $_GET['q'] = $uri;
+ protected $killme_stub;
+ protected $goaway_stub;
+
+ #[After]
+ public function cleanup_stubs(): void {
+ $this->killme_stub = null;
+ $this->goaway_stub = null;
+ }
- if (!empty($query)) {
- $_GET = array_merge($_GET, $query);
- }
+ 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'] = 'GET';
+ $_SERVER['REQUEST_METHOD'] = $method;
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
$_SERVER['QUERY_STRING'] = "q={$uri}";
+ $_SERVER['REQUEST_URI'] = $uri;
+
// phpcs:disable Generic.PHP.DisallowRequestSuperglobal.Found
- $_REQUEST = $_GET;
+ $_REQUEST = array_merge($_GET, $_POST);
// phpcs::enable
\App::init();
@@ -55,6 +57,32 @@ class TestCase extends UnitTestCase {
}
/**
+ * 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.
@@ -100,8 +128,7 @@ class TestCase extends UnitTestCase {
* @throws KillmeException
*/
protected function stub_killme(): void {
- $killme_stub = $this->getFunctionMock('Zotlabs\Module', 'killme');
- $killme_stub
+ $this->killme_stub = $this->getFunctionMock('Zotlabs\Module', 'killme')
->expects($this->once())
->willReturnCallback(
function () {
@@ -147,8 +174,7 @@ class TestCase extends UnitTestCase {
* @throws RedirectException
*/
protected function stub_goaway(): void {
- $goaway_stub = $this->getFunctionMock('Zotlabs\Module', 'goaway');
- $goaway_stub
+ $this->goaway_stub = $this->getFunctionMock('Zotlabs\Module', 'goaway')
->expects($this->once())
->willReturnCallback(
function (string $uri) {
diff --git a/tests/unit/Thumb/EpubthumbTest.php b/tests/unit/Thumb/EpubthumbTest.php
new file mode 100644
index 000000000..d381d940e
--- /dev/null
+++ b/tests/unit/Thumb/EpubthumbTest.php
@@ -0,0 +1,158 @@
+<?php
+/*
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Thumbs;
+
+use PHPUnit\Framework\Attributes\{AfterClass, Before, BeforeClass};
+use Zotlabs\Thumbs\Epubthumb;
+use Zotlabs\Tests\Unit\UnitTestCase;
+
+use ZipArchive;
+
+class EpubthumbTest extends UnitTestCase {
+ private const TMPDIR = __DIR__ . '/tmp';
+
+ private Epubthumb $thumbnailer;
+
+ /**
+ * Create a temp dir to use for the tests in this class.
+ */
+ #[BeforeClass]
+ static function setupTmpDir(): void {
+ if (!is_dir(self::TMPDIR)) {
+ mkdir(self::TMPDIR);
+ }
+ }
+
+ /**
+ * Clean up and remove the temp dir after the tests.
+ */
+ #[AfterClass]
+ static function cleanupTmpDir(): void {
+ $files = scandir(self::TMPDIR);
+ if ($files !== false) {
+ foreach($files as $f) {
+ if ($f[0] !== '.') {
+ unlink(self::TMPDIR . '/' . $f);
+ }
+ }
+ }
+ rmdir(self::TMPDIR);
+ }
+
+ /**
+ * Create the thumbnailer object for tests.
+ *
+ * This is run before each test, so that each test has it's own
+ * instance of the thumbnailer.
+ */
+ #[Before]
+ function createThumbnailer(): void {
+ $this->thumbnailer = new Epubthumb();
+ }
+
+ /*
+ * Tests
+ */
+
+ public function testEpubThumbMatch(): void {
+ $this->assertTrue($this->thumbnailer->Match('application/epub+zip'));
+ $this->assertFalse($this->thumbnailer->Match('application/zip'));
+ }
+
+ public function testNoThumbnailCreatedForFileThatDontExist(): void {
+ $this->checkCreateThumbnail(self::TMPDIR . '/nonexisting.epub', false);
+ }
+
+ public function testNoThumbnailCreatedIfNotAZipArchive(): void {
+ $filename = self::TMPDIR . '/notazip.epub';
+
+ file_put_contents($filename, 'This is not a ZIP file!');
+
+ $this->checkCreateThumbnail($filename, false);
+ }
+
+ public function testNoThumbnailCreatedIfInvalidEpub(): void {
+ $filename = self::TMPDIR . '/nocontainer.epub';
+
+ $epub = new ZipArchive();
+ $epub->open($filename, ZipArchive::CREATE);
+ $epub->addFromString('somefile.txt', 'It was a dark an stormy night...');
+ $epub->close();
+
+ $this->checkCreateThumbnail($filename, false);
+ }
+
+ public function testNoThumbnailCreatedIfCoverFileMissing(): void {
+ $filename = self::TMPDIR . '/good.epub';
+
+ $epub = new ZipArchive();
+ $epub->open($filename, ZipArchive::CREATE);
+ $this->addEpubContainer($epub);
+ $this->addEpubPackage($epub);
+ $epub->close();
+
+ $this->checkCreateThumbnail($filename, false);
+ }
+
+ public function testCreateCoverFromEpub(): void {
+ $filename = self::TMPDIR . '/good.epub';
+
+ $epub = new ZipArchive();
+ $epub->open($filename, ZipArchive::CREATE);
+ $this->addEpubContainer($epub);
+ $this->addEpubPackage($epub);
+ $epub->addFile(PROJECT_BASE . '/images/red-koala.png', 'EPUB/cover.png');
+ $epub->close();
+
+ $this->checkCreateThumbnail($filename, true);
+ }
+
+ /*
+ * Helper functions
+ */
+
+ private function checkCreateThumbnail(string $filename, bool $expectThumbnail): void {
+ $attach = [ 'content' => $filename ];
+ $this->thumbnailer->Thumb($attach, 0);
+
+ $this->assertEquals($expectThumbnail, file_exists($filename . '.thumb'));
+ }
+
+ private function addEpubContainer(ZipArchive $epub): void {
+ $xml = <<<XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
+ <rootfiles>
+ <rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/>
+ </rootfiles>
+ </container>
+ XML;
+
+ $epub->addEmptyDir('META-INF');
+ $epub->addFromString('META-INF/container.xml', $xml);
+ }
+
+ private function addEpubPackage(ZipArchive $epub): void {
+ $xml = <<<XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-identifier">
+ <manifest>
+ <item
+ properties="cover-image"
+ id="ci"
+ href="cover.png"
+ media-type="image/png" />
+ </manifest>
+ </package>
+ XML;
+
+ $epub->addEmptyDir('EPUB');
+ $epub->addFromString('EPUB/package.opf', $xml);
+ }
+}
diff --git a/tests/unit/UnitTestCase.php b/tests/unit/UnitTestCase.php
index afc309205..e3cd22b63 100644
--- a/tests/unit/UnitTestCase.php
+++ b/tests/unit/UnitTestCase.php
@@ -47,7 +47,7 @@ require_once 'include/dba/dba_transaction.php';
*/
class UnitTestCase extends TestCase {
protected array $fixtures = array();
- protected ?\DbaTransaction $db_transacton = null;
+ protected ?\DbaTransaction $db_transaction = null;
/**
* Connect to the test db, load fixtures and global config.
@@ -75,6 +75,14 @@ class UnitTestCase extends TestCase {
}
/**
+ * Initialize the global App properties.
+ */
+ #[Before]
+ protected function init_app(): void {
+ \App::set_hostname('hubzilla.test');
+ }
+
+ /**
* Roll back test database to it's original state, cleaning up
* any changes from the test.
*
diff --git a/tests/unit/Widget/HelpindexTest.php b/tests/unit/Widget/HelpindexTest.php
index 26aa34104..87042c559 100644
--- a/tests/unit/Widget/HelpindexTest.php
+++ b/tests/unit/Widget/HelpindexTest.php
@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/
+use PHPUnit\Framework\Attributes\Before;
+
/**
* Test class for testing the Helpindex widget.
*/
@@ -15,6 +17,8 @@ class HelpindexTest extends \Zotlabs\Tests\Unit\Module\TestCase {
use \phpmock\phpunit\PHPMock;
+ private string $output;
+
/**
* Define the stubs to make sure they work later in the test.
*
@@ -27,6 +31,12 @@ class HelpindexTest extends \Zotlabs\Tests\Unit\Module\TestCase {
self::defineFunctionMock('Zotlabs\Widget', 'file_get_contents');
}
+ #[Before]
+ public function setup_state(): void {
+ // Make sure the output is cleared before running the test
+ $this->output = '';
+ }
+
public function test_loading_toc(): void {
// Stub `file_get_contents` to plant our own content.
$fgc_stub = $this->getFunctionMock('Zotlabs\Widget', 'file_get_contents');
diff --git a/tests/unit/Widget/MessagesWidgetTest.php b/tests/unit/Widget/MessagesWidgetTest.php
new file mode 100644
index 000000000..ca025ff43
--- /dev/null
+++ b/tests/unit/Widget/MessagesWidgetTest.php
@@ -0,0 +1,83 @@
+<?php
+/*
+ * SPDX-FileCopyrightText: 2025 The Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\Widget;
+
+use App;
+use Zotlabs\Widget\Messages;
+use Zotlabs\Tests\Unit\Module\TestCase;
+
+class MessagesWidgetTest extends TestCase
+{
+ use \phpmock\phpunit\PHPMock;
+
+ /**
+ * List of file tags should be empty if there are no file tags.
+ */
+ public function testNoFileTags(): void
+ {
+ $local_channe_stub = $this->getFunctionMock('Zotlabs\Widget', 'local_channel')
+ ->expects($this->any())
+ ->willReturn(42);
+
+ $feature_enabled_stub = $this->getFunctionMock('Zotlabs\Widget', 'feature_enabled')
+ ->expects($this->any())
+ ->willReturn(true);
+
+ $this->renderWidget();
+ $this->assertOutputMatches('|<datalist\s+id="data_filetags">\s+</datalist>|');
+ }
+
+ /**
+ * The widget lists file tags that are defined for the channel.
+ */
+ public function testFileTagsAreListed(): void
+ {
+ $local_channe_stub = $this->getFunctionMock('Zotlabs\Widget', 'local_channel')
+ ->expects($this->any())
+ ->willReturn(42);
+
+ $feature_enabled_stub = $this->getFunctionMock('Zotlabs\Widget', 'feature_enabled')
+ ->expects($this->any())
+ ->willReturn(true);
+
+ /*
+ * Add a few tags.
+ */
+ store_item_tag(42, 1, TERM_OBJ_POST, TERM_FILE, 'test_file_tag', '');
+ store_item_tag(42, 1, TERM_OBJ_POST, TERM_FILE, 'test_file_tag2', '');
+
+ $this->renderWidget();
+ $this->assertOutputMatches('|<option\s+value="test_file_tag">|');
+ $this->assertOutputMatches('|<option\s+value="test_file_tag2">|');
+ }
+
+ /**
+ * Initializes the app and calls the widget code.
+ */
+ private function renderWidget(): void {
+ $_GET['q'] = 'hq';
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+
+ App::init();
+
+ $widget = new Messages();
+ $this->output = $widget->widget([]);
+ }
+
+ /**
+ * Asserts that the output matches a given regex pattern.
+ *
+ * If the pattern does not match, the test will be marked as failed.
+ *
+ * @param string $pattern The regex that should be matched.
+ */
+ private function assertOutputMatches(string $pattern): void {
+ $this->assertMatchesRegularExpression($pattern, $this->output);
+ }
+}
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
new file mode 100644
index 000000000..296e1b9b6
--- /dev/null
+++ b/tests/unit/bootstrap.php
@@ -0,0 +1,9 @@
+<?php
+/**
+ * Bootstrapping unit test framework
+ *
+ */
+
+require_once __dir__ . '/../../boot.php';
+require_once __dir__ . '/UnitTestCase.php';
+require_once __dir__ . '/Module/TestCase.php';
diff --git a/tests/unit/includes/AccountTest.php b/tests/unit/includes/AccountTest.php
index 3978f9d04..66c761ef5 100644
--- a/tests/unit/includes/AccountTest.php
+++ b/tests/unit/includes/AccountTest.php
@@ -1,9 +1,28 @@
<?php
+
+use Zotlabs\Tests\Unit\UnitTestCase;
+
/**
* Tests for account handling helper functions.
*/
+class AccountTest extends UnitTestCase {
+
+ /**
+ * Test the `get_account_id()` function.
+ */
+ public function test_get_account_id() {
+ App::set_account(null);
+ unset($_SESSION['account_id']);
+
+ $this->assertEquals(false, get_account_id(), 'get_account_id() should return false if not authenticated');
+
+ App::set_account(['account_id' => 36]);
+ $this->assertEquals(36, get_account_id(), 'get_account_id() should return account from global App object');
+
+ $_SESSION['account_id'] = 42;
+ $this->assertEquals(42, get_account_id(), 'get_account_id() should return the account from the session');
+ }
-class AccountTest extends Zotlabs\Tests\Unit\UnitTestCase {
public function test_get_account_by_id_returns_existing_account() {
$account = get_account_by_id(42);
$this->assertNotFalse($account);
diff --git a/tests/unit/includes/BBCodeTest.php b/tests/unit/includes/BBCodeTest.php
index daa66bf72..982ef4eb9 100644
--- a/tests/unit/includes/BBCodeTest.php
+++ b/tests/unit/includes/BBCodeTest.php
@@ -23,6 +23,7 @@
namespace Zotlabs\Tests\Unit\includes;
+use App;
use Zotlabs\Tests\Unit\UnitTestCase;
class BBCodeTest extends UnitTestCase {
@@ -42,7 +43,7 @@ class BBCodeTest extends UnitTestCase {
*/
public function test_bbcode_observer(string $src, bool $logged_in, string $lang, string $expected): void {
if ($logged_in) {
- \App::$observer = [
+ App::set_observer([
'xchan_addr' => '',
'xchan_name' => '',
'xchan_connurl' => '',
@@ -50,9 +51,9 @@ class BBCodeTest extends UnitTestCase {
// port required in xchan url due to bug in get_rpost_path
'xchan_url' => 'https://example.com:666',
- ];
+ ]);
} else {
- \App::$observer = null;
+ App::set_observer(null);
}
\App::$language = $lang;
@@ -138,7 +139,39 @@ class BBCodeTest extends UnitTestCase {
'del tag' => [
'some [s]strike through[/s] text',
'some <del>strike through</del> text'
- ]
+ ],
+ 'naked url is converted to link' => [
+ 'example url: https://example.com',
+ 'example url: <a href="https://example.com" target="_blank" rel="nofollow noopener">https://example.com</a>'
+ ],
+ 'naked url followed by newline' => [
+ "https://www.example.com\nhave a great day.",
+ '<a href="https://www.example.com" target="_blank" rel="nofollow noopener">https://www.example.com</a><br />have a great day.',
+ ],
+ 'inline naked url' => [
+ "This is a link https://example.com/some/path more info.",
+ 'This is a link <a href="https://example.com/some/path" target="_blank" rel="nofollow noopener">https://example.com/some/path</a> more info.',
+ ],
+ 'naked url within code block is not converted to link' => [
+ "[code]\nhttp://example.com\n[/code]",
+ "<pre><code>http://example.com</code></pre>"
+ ],
+ 'geo uri is converted to link' => [
+ 'example url: [url]geo:37.786971,-122.399677;u=35[/url]',
+ 'example url: <a href="geo:37.786971,-122.399677;u=35" target="_blank" rel="nofollow noopener">geo:37.786971,-122.399677;u=35</a>'
+ ],
+ 'geo uri with label is converted to link' => [
+ 'example url: [url=geo:37.786971,-122.399677;u=35(Wikimedia+Foundation)]Wikimedia Foundation[/url]',
+ 'example url: <a href="geo:37.786971,-122.399677;u=35(Wikimedia+Foundation)" target="_blank" rel="nofollow noopener">Wikimedia Foundation</a>'
+ ],
+ 'naked geo uri is converted to link' => [
+ 'example url: geo:37.786971,-122.399677;u=35',
+ 'example url: <a href="geo:37.786971,-122.399677;u=35" target="_blank" rel="nofollow noopener">geo:37.786971,-122.399677;u=35</a>'
+ ],
+ 'naked geo uri with label is converted to link' => [
+ 'example url: geo:37.78918,-122.40335(Wikimedia+Foundation)',
+ 'example url: <a href="geo:37.78918,-122.40335(Wikimedia+Foundation)" target="_blank" rel="nofollow noopener">📍Wikimedia Foundation</a>'
+ ],
];
}
@@ -189,7 +222,7 @@ class BBCodeTest extends UnitTestCase {
'[rpost=a title]This is the body[/rpost]',
true,
'en',
- '<a href="https://example.com:666/rpost?f=&title=a+title&body=This+is+the+body" target="_blank" rel="nofollow noopener">https://example.com:666/rpost?f=&title=a+title&body=This+is+the+body</a>',
+ '<a href="https://example.com:666/rpost?f=&title=a+title&body=This+is+the+body" target="_blank" rel="nofollow noopener">https://example.com:666/rpost?f=&title=a+title&body=This+is+the+body</a>',
],
'unauthenticated observer rpost' => [
'[rpost=a title]This is the body[/rpost]',
@@ -223,6 +256,10 @@ class BBCodeTest extends UnitTestCase {
"<pre><code>some\n indented\ncode</code></pre>",
"[code]some\n indented\ncode[/code]"
],
+ 'code block with URL' => [
+ '<pre><code>\nproxy_pass http://example.com\n</code></pre>',
+ '[code]\nproxy_pass http://example.com\n[/code]'
+ ],
'paragraph with a mention and some text' => [
'<p><span class="h-card" translate="no"><a href="https://example.org/@profile" class="u-url mention">@<span>profile</span></a></span> some content</p>',
'[url=https://example.org/@profile]@profile[/url] some content'
@@ -266,6 +303,10 @@ class BBCodeTest extends UnitTestCase {
'del tag' => [
'some <del>strike through</del> text',
'some [s]strike through[/s] text'
+ ],
+ 'table' => [
+ '<table><tr><td>row1, col1</td><td>row1, col2</td></tr><tr><td>row2, col1</td><td>row2, col2</td></tr></table>',
+ '[table][tr][td]row1, col1[/td][td]row1, col2[/td][/tr][tr][td]row2, col1[/td][td]row2, col2[/td][/tr][/table]'
]
];
}
diff --git a/tests/unit/includes/DatetimeTest.php b/tests/unit/includes/DatetimeTest.php
new file mode 100644
index 000000000..f8c480449
--- /dev/null
+++ b/tests/unit/includes/DatetimeTest.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * tests function from include/datetime.php
+ *
+ * @package test.util
+ */
+
+use Zotlabs\Tests\Unit\UnitTestCase;
+
+class DatetimeTest extends UnitTestCase {
+
+ // Test when the timestamp is in the past
+ public function test_relative_time_past() {
+ $now = new DateTime('2024-12-07 00:00:00');
+ $timestamp = datetime_convert(date_default_timezone_get(), 'UTC', '2023-12-05 10:30:00');
+ $result = relative_time($timestamp, $now);
+ $this->assertEquals('1 year ago', $result);
+ }
+
+ // Test when the timestamp is in the future
+ public function test_relative_time_future() {
+ $now = new DateTime('2024-12-07 00:00:00');
+ $timestamp = datetime_convert(date_default_timezone_get(), 'UTC', '2024-12-09 12:00:00');
+ $result = relative_time($timestamp, $now);
+ $this->assertEquals('in 2 days', $result);
+ }
+
+ // Test for "now" case (timestamp exactly equal to current time)
+ public function test_relative_time_now() {
+ $now = new DateTime('2024-12-07 00:00:00');
+ $timestamp = datetime_convert(date_default_timezone_get(), 'UTC', '2024-12-07 00:00:00');
+ $result = relative_time($timestamp, $now);
+ $this->assertEquals('now', $result);
+ }
+
+ // Test for future time with smaller units (e.g., minutes)
+ public function test_relative_time_future_minutes() {
+ $now = new DateTime('2024-12-07 10:30:00');
+ $timestamp = datetime_convert(date_default_timezone_get(), 'UTC', '2024-12-07 10:35:00');
+ $result = relative_time($timestamp, $now);
+ $this->assertEquals('in 5 minutes', $result);
+ }
+
+ // Test for past time with smaller units (e.g., seconds)
+ public function test_relative_time_past_seconds() {
+ $now = new DateTime('2024-12-07 10:30:00');
+ $timestamp = datetime_convert(date_default_timezone_get(), 'UTC', '2024-12-07 10:29:58');
+ $result = relative_time($timestamp, $now);
+ $this->assertEquals('2 seconds ago', $result);
+ }
+}
+
+
diff --git a/tests/unit/includes/ItemsTest.php b/tests/unit/includes/ItemsTest.php
new file mode 100644
index 000000000..1c2fb6725
--- /dev/null
+++ b/tests/unit/includes/ItemsTest.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * tests function from include/items.php
+ *
+ * @package test.util
+ */
+
+use Zotlabs\Tests\Unit\UnitTestCase;
+
+class ItemsTest extends UnitTestCase {
+ /**
+ * Data provider for item_forwardable function.
+ *
+ * @return array
+ */
+ public static function itemForwardableDataProvider()
+ {
+ return [
+ // Test case: item is unpublished
+ [
+ [
+ 'item_unpublished' => 1,
+ 'item_delayed' => 0,
+ 'item_blocked' => 0,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Create',
+ 'postopts' => '',
+ 'author' => ['xchan_network' => '']
+ ],
+ false // Expected result
+ ],
+ // Test case: item is delayed
+ [
+ [
+ 'item_unpublished' => 0,
+ 'item_delayed' => 1,
+ 'item_blocked' => 0,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Create',
+ 'postopts' => '',
+ 'author' => ['xchan_network' => '']
+ ],
+ false
+ ],
+ // Test case: item is blocked
+ [
+ [
+ 'item_unpublished' => 0,
+ 'item_delayed' => 0,
+ 'item_blocked' => 1,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Create',
+ 'postopts' => '',
+ 'author' => ['xchan_network' => '']
+ ],
+ false
+ ],
+ // Test case: verb is 'Follow' (forbidden verb)
+ [
+ [
+ 'item_unpublished' => 0,
+ 'item_delayed' => 0,
+ 'item_blocked' => 0,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Follow',
+ 'postopts' => '',
+ 'author' => ['xchan_network' => '']
+ ],
+ false
+ ],
+ // Test case: postopts contains 'nodeliver'
+ [
+ [
+ 'item_unpublished' => 0,
+ 'item_delayed' => 0,
+ 'item_blocked' => 0,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Create',
+ 'postopts' => 'nodeliver',
+ 'author' => ['xchan_network' => '']
+ ],
+ false
+ ],
+ // Test case: actor's network is 'rss' (restricted network)
+ [
+ [
+ 'item_unpublished' => 0,
+ 'item_delayed' => 0,
+ 'item_blocked' => 0,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Create',
+ 'postopts' => '',
+ 'author' => ['xchan_network' => 'rss']
+ ],
+ false
+ ],
+ // Test case: no conditions met (should forward)
+ [
+ [
+ 'item_unpublished' => 0,
+ 'item_delayed' => 0,
+ 'item_blocked' => 0,
+ 'item_hidden' => 0,
+ 'item_restrict' => 0,
+ 'verb' => 'Create',
+ 'postopts' => '',
+ 'author' => ['xchan_network' => 'other']
+ ],
+ true
+ ]
+ ];
+ }
+
+ /**
+ * Test item_forwardable with various data.
+ *
+ * @dataProvider itemForwardableDataProvider
+ */
+ public function testItemForwardable($item, $expected)
+ {
+ $this->assertSame($expected, item_forwardable($item));
+ }
+
+}
+
+
diff --git a/tests/unit/includes/LanguageTest.php b/tests/unit/includes/LanguageTest.php
index 8f62e71e2..90249c5c8 100644
--- a/tests/unit/includes/LanguageTest.php
+++ b/tests/unit/includes/LanguageTest.php
@@ -86,16 +86,16 @@ class LanguageTest extends UnitTestCase {
'en-gb',
'British English',
[
- 'de' => 'Englisch (Vereinigtes Königreich)',
- 'nb' => 'engelsk (Storbritannia)'
+ 'de' => 'British English', // should be Englisch (Vereinigtes Königreich), seems to be a bug upstream
+ 'nb' => 'British English' // should be engelsk (Storbritannia), seems to be a bug upstream
]
],
'en-au' => [
'en-au',
'Australian English',
[
- 'de' => 'Englisch (Australien)',
- 'nb' => 'engelsk (Australia)'
+ 'de' => 'Australian English', // should be Englisch (Australien), seems to be a bug upstream
+ 'nb' => 'Australian English' // should be engelsk (Australia), , seems to be a bug upstream
]
],
'nb' => [
diff --git a/tests/unit/includes/MarkdownTest.php b/tests/unit/includes/MarkdownTest.php
index 310130bf1..55dbb4445 100644
--- a/tests/unit/includes/MarkdownTest.php
+++ b/tests/unit/includes/MarkdownTest.php
@@ -36,7 +36,7 @@ class MarkdownTest extends UnitTestCase {
* @dataProvider markdown_to_bbcode_provider
*/
public function test_markdown_to_bbcode(string $expected, string $src): void {
- $this->assertEquals($expected, markdown_to_bb($src));
+ $this->assertEquals($expected, markdown_to_bb($src, true, ['preserve_lf' => true]));
}
public static function markdown_to_bbcode_provider(): array {
@@ -54,11 +54,14 @@ class MarkdownTest extends UnitTestCase {
'This is a test of **bold text**, *italic text* and ***bold and italic text***'
],
'multiline text' => [
- 'This text is text wrapped over multiple lines.',
+ // This is not as expected in markdown, but may be needed
+ // for compatibility with bbcode behaviour.
+ "This text is\ntext wrapped\nover multiple\nlines.",
"This text is\ntext wrapped\nover multiple\nlines."
],
'text with hard linebreak' => [
- "Line one\nLine two",
+ // An extra line break is inserted here...
+ "Line one\n\nLine two",
"Line one \nLine two"
],
'paragraphs' => [
@@ -78,21 +81,39 @@ class MarkdownTest extends UnitTestCase {
'`some code`'
],
'inline code with wrapped text' => [
- '[code]some code unwrapped[/code]',
+ // Not sure if the newline should be preseved here?
+ "[code]some code\nunwrapped[/code]",
"`some code\n unwrapped`"
],
'code block no language' => [
- "[code]some code\nover multiple lines[/code]",
+ "[code]some code\nover multiple lines\n[/code]",
"```\nsome code\nover multiple lines\n```"
],
'code block no language indented' => [
- "[code]some code\n over multiple lines\n with indentation[/code]",
+ // For some reason one space char is eaten on indented lines.
+ "[code]some code\n over multiple lines\n with indentation\n[/code]",
"```\nsome code\n over multiple lines\n with indentation\n```"
],
'code block with language' => [
- "[code=php]&lt;?php\necho phpinfo();[/code]",
+ "[code=php]&lt;?php\necho phpinfo();\n[/code]",
"```php\n<?php\necho phpinfo();\n```"
],
+ 'code block with URL' => [
+ "[code]an example url https://example.com\n[/code]",
+ "```\nan example url https://example.com\n```"
+ ],
+ 'bbcode code block with URL' => [
+ "[code]\nproxy_pass http://example.com;\n[/code]",
+ "[code]\nproxy_pass http://example.com;\n[/code]"
+ ],
+ 'naked url followed by newline' => [
+ "https://example.com\nhave a great day.",
+ "https://example.com\nhave a great day.",
+ ],
+ 'inline naked url' => [
+ 'This is a link https://example.com/some/path more info.',
+ 'This is a link https://example.com/some/path more info.',
+ ],
];
}
diff --git a/tests/unit/includes/NetworkTest.php b/tests/unit/includes/NetworkTest.php
index a41075f25..0d99fc9c3 100644
--- a/tests/unit/includes/NetworkTest.php
+++ b/tests/unit/includes/NetworkTest.php
@@ -60,7 +60,63 @@ class NetworkTest extends Zotlabs\Tests\Unit\UnitTestCase {
['some.email@example.cancerresearch', true],
// And internationalized TLD's
- ['some.email@example.شبكة', true]
+ ['some.email@example.شبكة', true],
+
+ // Allow plus/minus addressing
+ ['address+tag@example.com', true],
+ ['address-tag@example.com', true],
+ ];
+ }
+
+ /**
+ * Test the unparse_url function.
+ *
+ */
+ public function test_unparse_url_full()
+ {
+ $parsed_url = [
+ 'scheme' => 'https',
+ 'host' => 'www.example.com',
+ 'port' => '8080',
+ 'user' => 'username',
+ 'pass' => 'password',
+ 'path' => '/path',
+ 'query' => 'param=value',
+ 'fragment' => 'section'
+ ];
+
+ $expected = 'https://username:password@www.example.com:8080/path?param=value#section';
+ $this->assertEquals($expected, unparse_url($parsed_url));
+ }
+
+ public function test_unparse_url_partial()
+ {
+ $parsed_url = [
+ 'scheme' => 'http',
+ 'host' => 'example.com',
+ 'path' => '/index.php'
];
+
+ $expected = 'http://example.com/index.php';
+ $this->assertEquals($expected, unparse_url($parsed_url));
+ }
+
+ public function test_unparse_url_custom()
+ {
+ $parsed_url = [
+ 'scheme' => 'https',
+ 'host' => 'www.example.com',
+ 'port' => '443',
+ 'path' => '/api'
+ ];
+
+ $parts = ['scheme', 'host'];
+ $expected = 'https://www.example.com';
+ $this->assertEquals($expected, unparse_url($parsed_url, $parts));
+ }
+
+ public function test_unparse_url_empty()
+ {
+ $this->assertEquals('', unparse_url([]));
}
}
diff --git a/tests/unit/includes/PhotodriverTest.php b/tests/unit/includes/PhotodriverTest.php
index 34dc058b7..db9883589 100644
--- a/tests/unit/includes/PhotodriverTest.php
+++ b/tests/unit/includes/PhotodriverTest.php
@@ -20,4 +20,62 @@ class PhotodriverTest extends UnitTestCase {
$photo = \photo_factory(file_get_contents('images/hz-16.png'), 'image/png');
$this->assertInstanceOf('Zotlabs\Photo\PhotoGd', $photo);
}
+
+ // Helper to create a temporary image file
+ private function createTempImage($type = 'jpeg'): string
+ {
+ $tmp = tempnam(sys_get_temp_dir(), 'img');
+ switch ($type) {
+ case 'png':
+ $im = imagecreatetruecolor(10, 10);
+ imagepng($im, $tmp);
+ imagedestroy($im);
+ break;
+ case 'jpeg':
+ default:
+ $im = imagecreatetruecolor(10, 10);
+ imagejpeg($im, $tmp);
+ imagedestroy($im);
+ break;
+ }
+ return $tmp;
+ }
+
+ public function testGuessImageTypeFromRawData()
+ {
+ $filename = 'irrelevant';
+ $data = [
+ 'body' => file_get_contents($this->createTempImage('jpeg'))
+ ];
+ $result = guess_image_type($filename, $data);
+ $this->assertEquals('image/jpeg', $result);
+ }
+
+ public function testGuessImageTypeFromLocalFile()
+ {
+ $file = $this->createTempImage('png');
+ $result = guess_image_type($file);
+ $this->assertEquals('image/png', $result);
+ unlink($file);
+ }
+
+ public function testGuessImageTypeFromHeaders()
+ {
+ $filename = 'irrelevant';
+ $data = [
+ 'header' => "Content-Type: image/jpeg\nOther: value"
+ ];
+ $result = guess_image_type($filename, $data);
+ $this->assertEquals('image/jpeg', $result);
+ }
+
+ public function testGuessImageTypeUnknownTypeReturnsNull()
+ {
+ $filename = 'not_an_image.txt';
+ $data = [
+ 'body' => 'not an image'
+ ];
+ $result = guess_image_type($filename, $data);
+ $this->assertNull($result);
+ }
}
diff --git a/tests/unit/includes/dba/DbaPdoTest.php b/tests/unit/includes/dba/DbaPdoTest.php
new file mode 100644
index 000000000..8a1a2b197
--- /dev/null
+++ b/tests/unit/includes/dba/DbaPdoTest.php
@@ -0,0 +1,140 @@
+<?php
+/**
+ * Tests for `includes/dba_pdo.php`.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+namespace Zotlabs\Tests\Unit\includes;
+
+use DBA;
+use PDO;
+use PDOStatement;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Zotlabs\Tests\Unit\UnitTestCase;
+
+class DbaPdoTest extends UnitTestCase
+{
+ public function testInsertingRowWithRturningClauseReturnsInsertedRow(): void
+ {
+ // MySQL does not support the `returning` clause, so we skip the test
+ // for that DB backend.
+ $this->skipIfMySQL();
+
+ // Let's manually insert a row in the config table.
+ // This is just because it's a conventient table to test
+ // against
+ $res = q(<<<SQL
+ INSERT INTO config (cat, k, v)
+ VALUES ('test', 'a key', 'A value')
+ RETURNING *
+ SQL);
+
+ $this->assertIsArray($res);
+ $this->assertIsArray($res[0]);
+ $this->assertTrue($res[0]['id'] > 0);
+ $this->assertEquals('test', $res[0]['cat']);
+ $this->assertEquals('a key', $res[0]['k']);
+ $this->assertEquals('A value', $res[0]['v']);
+ }
+
+ #[DataProvider('insertRowProvider')]
+ public function testInsertRow(string $table, array $data, string $id): void
+ {
+ $res = DBA::$dba->insert($table, $data, $id);
+
+ $this->assertIsArray($res);
+
+ // Make sure the result contains the expected id
+ $this->assertArrayHasKey($id, $res);
+
+ foreach ($data as $key => $value) {
+ $this->assertEquals($value, $res[$key]);
+ }
+ }
+
+ #[DataProvider('insertRowProvider')]
+ public function testInsertShouldReturnFalseIfInsertFails(
+ string $table,
+ array $data,
+ string $id
+ ): void
+ {
+ $res1 = DBA::$dba->insert($table, $data, $id);
+ $this->assertIsArray($res1);
+
+ // Inserting the same row again should fail.
+ $res2 = DBA::$dba->insert($table, $data, $id);
+ $this->assertFalse($res2);
+ }
+
+ /**
+ * Dataprovider for testInertRow.
+ *
+ * @return array An array of [ $table, $data, $id ] elements.
+ */
+ public static function insertRowProvider(): array
+ {
+ return [
+ 'table with numeric primary id' => [
+ 'config',
+ [ 'cat' => 'test', 'k' => 'a key', 'v' => 'A value' ],
+ 'id',
+ ],
+ 'table with text primary id' => [
+ 'cache',
+ [ 'k' => 'some key', 'v' => 'cached value', 'updated' => date('Y-m-d H:i:s')],
+ 'k',
+ ],
+ ];
+ }
+
+ public function testUpdateRow(): void
+ {
+ // Let's fetch a row from the config table
+ $res = q("SELECT * FROM config WHERE cat = 'system' AND k = 'baseurl'");
+
+ $this->assertIsArray($res);
+ $this->assertIsArray($res[0]);
+
+ $row = $res[0];
+
+ // Update the baseurl
+ $updated = DBA::$dba->update(
+ 'config',
+ [ 'v' => 'https://some.other_site.test/' ],
+ 'id',
+ $row['id']
+ );
+
+ $this->assertTrue($updated);
+
+ // Verify that the record was updated
+ $updated_res = q("SELECT * FROM config WHERE cat = 'system' AND k = 'baseurl'");
+ $this->assertIsArray($updated_res);
+
+ $updated_row = $updated_res[0];
+
+ $this->assertIsArray($updated_row);
+ $this->assertEquals($row['id'], $updated_row['id']);
+ $this->assertEquals('system', $updated_row['cat']);
+ $this->assertEquals('baseurl', $updated_row['k']);
+ $this->assertEquals('https://some.other_site.test/', $updated_row['v']);
+ }
+
+ /**
+ * Mark the test as skipped if the current db is MySQL.
+ */
+ private function skipIfMySQL(): void {
+ $driver = DBA::$dba->db->getAttribute(PDO::ATTR_DRIVER_NAME);
+ $version = DBA::$dba->db->getAttribute(PDO::ATTR_SERVER_VERSION);
+
+ if ($driver === 'mysql' && stripos($version, 'mariadb') === false) {
+ $this->markTestSkipped("RETURNING clause not supported for {$driver}");
+ }
+
+ }
+}
diff --git a/tests/unit/includes/dba/_files/account.yml b/tests/unit/includes/dba/_files/account.yml
index 88e59056e..9c3d00ec8 100644
--- a/tests/unit/includes/dba/_files/account.yml
+++ b/tests/unit/includes/dba/_files/account.yml
@@ -3,9 +3,26 @@ account:
account_id: 42
account_email: "hubzilla@example.com"
account_language: "no"
+ account_level: 5
account_flags: 0
-
account_id: 43
account_email: "hubzilla@example.org"
account_language: "de"
+ account_level: 5
account_flags: 1
+ -
+ account_id: 44
+ account_email: "blocked@example.org"
+ account_level: 5
+ account_flags: 2
+ -
+ account_id: 45
+ account_email: "pending@example.org"
+ account_level: 5
+ account_flags: 0x10
+ -
+ account_id: 46
+ account_email: "unverified@example.org"
+ account_level: 5
+ account_flags: 0x11
diff --git a/tests/unit/includes/dba/_files/register.yml b/tests/unit/includes/dba/_files/register.yml
new file mode 100644
index 000000000..2ef1a5365
--- /dev/null
+++ b/tests/unit/includes/dba/_files/register.yml
@@ -0,0 +1,20 @@
+---
+register:
+ -
+ reg_vital: 1
+ reg_flags: 0x10
+ reg_did2: 'verified@example.com'
+ reg_email: 'verified@example.com'
+ reg_hash: '123'
+ reg_uid: 45
+ reg_pass: 'verify'
+ reg_stuff: ''
+ -
+ reg_vital: 1
+ reg_flags: 0x11
+ reg_did2: 'unverified@example.com'
+ reg_email: 'unverified@example.com'
+ reg_hash: '666'
+ reg_uid: 46
+ reg_pass: 'verify'
+ reg_stuff: ''