/ tests / GroupModelTest.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see
 * <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @package seek_quarry\tests
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\models\GroupModel;
use seekquarry\yioop\models\Model;
use seekquarry\yioop\models\ProfileModel;
use seekquarry\yioop\models\datasources\Sqlite3Manager;

/**
 * Checks what GroupModel counts and what it lets go of. The counting
 * cases stand up groups, members, wiki pages, threads and posts. The
 * letting-go cases stand up one conversation, kept twice as a message
 * between two people is, and ask who may let go of which copy.
 *
 * @author Chris Pollett
 */
class GroupModelTest extends UnitTest
{
    /**
     * Folder under the test files the throwaway database is built in.
     */
    const TEST_DIR = '/test_files/group_model_message_delete';
    /**
     * The group a message's writer keeps their own copy in.
     */
    const WRITER_GROUP = 60;
    /**
     * The group the person written to keeps their copy in.
     */
    const READER_GROUP = 70;
    /**
     * The thread both copies of a conversation hang from.
     */
    const THREAD = 500;
    /**
     * Who wrote the messages.
     */
    const WRITER = 3;
    /**
     * Who was written to.
     */
    const READER = 4;
    /**
     * A throwaway Sqlite3 database built from ProfileModel definitions.
     * @var object
     */
    public $db;
    /**
     * File path of the throwaway database.
     * @var string
     */
    public $db_path;
    /**
     * Folder the throwaway database sits in.
     * @var string
     */
    public $where;
    /**
     * GroupModel wired to the throwaway database.
     * @var object
     */
    public $group_model;
    /**
     * Builds a throwaway database and stands up both what the counting
     * cases count and one conversation for the letting-go cases. Each
     * case gets a database of its own, so what one case lets go of
     * cannot be missing from the next.
     */
    public function setUp()
    {
        $this->where = __DIR__ . self::TEST_DIR;
        if (!file_exists($this->where)) {
            mkdir($this->where, 0777, true);
        }
        $this->db_path = $this->where . "/group" . uniqid() . ".db";
        $this->db = new Sqlite3Manager();
        $this->db->connect("", "", "", $this->db_path);
        $dbinfo = ["DBMS" => "Sqlite3", "DB_HOST" => ""];
        $profile = new ProfileModel(C\DB_NAME, false);
        $profile->initializeSql($this->db, $dbinfo);
        foreach (['SOCIAL_GROUPS', 'USER_GROUP', 'GROUP_PAGE',
            'GROUP_ITEM', 'ITEM_IMPRESSION_STAT',
            'ITEM_IMPRESSION_SUMMARY'] as $table) {
            $this->db->execute($profile->create_statements[$table]);
        }
        $this->group_model = new GroupModel(C\DB_NAME, false);
        $this->group_model->db = $this->db;
        /* user 5 owns groups 100 and 101, user 6 owns group 102 */
        foreach ([[100, 5], [101, 5], [102, 6]] as $group) {
            $this->db->execute("INSERT INTO SOCIAL_GROUPS (GROUP_ID, " .
                "GROUP_NAME, OWNER_ID) VALUES (?, ?, ?)",
                [$group[0], "Group " . $group[0], $group[1]]);
        }
        /* group 100 has three members */
        foreach ([5, 6, 7] as $member) {
            $this->group_model->addUserGroup($member, 100);
        }
        /* group 100 has two wiki pages */
        foreach ([1, 2] as $page_id) {
            $this->db->execute("INSERT INTO GROUP_PAGE (ID, GROUP_ID, " .
                "TITLE) VALUES (?, ?, ?)", [$page_id, 100, "Page $page_id"]);
        }
        /* group 100 has two threads (a top-level item has PARENT_ID = ID);
           the first thread has two replies, so it holds three posts */
        $rows = [[1, 1], [2, 2], [3, 1], [4, 1]];
        foreach ($rows as $row) {
            $this->db->execute("INSERT INTO GROUP_ITEM (ID, PARENT_ID, " .
                "GROUP_ID, USER_ID, TITLE, DESCRIPTION) " .
                "VALUES (?, ?, ?, ?, ?, ?)",
                [$row[0], $row[1], 100, 5, "Item " . $row[0], "body"]);
        }
        /* one conversation: a row it hangs from in each person's group,
           carrying no words, and two messages the writer sent, each
           kept once in each group */
        $this->addItem(self::THREAD, self::WRITER_GROUP, self::WRITER,
            self::THREAD, "");
        $this->addItem(self::THREAD + 10, self::READER_GROUP,
            self::READER, self::THREAD, "");
        $this->addItem(501, self::WRITER_GROUP, self::WRITER, self::THREAD);
        $this->addItem(502, self::READER_GROUP, self::WRITER, self::THREAD);
        $this->addItem(503, self::WRITER_GROUP, self::WRITER, self::THREAD);
        $this->addItem(504, self::READER_GROUP, self::WRITER, self::THREAD);
    }
    /**
     * Puts one message row in the throwaway database.
     *
     * @param int $item_id the row's own id
     * @param int $group_id whose group the row sits in
     * @param int $user_id who wrote it
     * @param int $parent_id the thread it hangs from
     * @param string $description what the message says, empty for the
     *      row a conversation hangs from
     */
    private function addItem($item_id, $group_id, $user_id, $parent_id,
        $description = "a message")
    {
        $this->db->execute("INSERT INTO GROUP_ITEM (ID, PARENT_ID, " .
            "GROUP_ID, USER_ID, TITLE, DESCRIPTION, PUBDATE) " .
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            [$item_id, $parent_id, $group_id, $user_id, "3-4",
            $description, 1000]);
    }
    /**
     * Says how many rows are left in a given group.
     *
     * @param int $group_id whose group to count
     * @return int how many rows it holds
     */
    private function countIn($group_id)
    {
        $result = $this->db->execute("SELECT COUNT(*) AS NUM FROM " .
            "GROUP_ITEM WHERE GROUP_ID=?", [$group_id]);
        $row = $this->db->fetchArray($result);
        return intval($row['NUM']);
    }
    /**
     * Takes down the throwaway database and the folder it sat in.
     */
    public function tearDown()
    {
        if ($this->db) {
            $this->db->disconnect();
        }
        if ($this->where && file_exists($this->where)) {
            $model = new Model();
            $model->db->unlinkRecursive($this->where);
        }
    }
    /**
     * The person written to can let go of their own copy of a message
     * someone else wrote, which is what lets a message stay gone from
     * one list while the other keeps it.
     */
    public function readerMayLetGoOfTheirOwnCopyTestCase()
    {
        $gone = $this->group_model->deleteMessageCopy(502, self::READER,
            self::READER_GROUP);
        $this->assertEqual($gone, 1,
            "the person written to lets go of their copy");
        $this->assertEqual($this->countIn(self::READER_GROUP), 2,
            "one row fewer is left in their group");
        $this->assertEqual($this->countIn(self::WRITER_GROUP), 3,
            "and the writer's copy is left where it was");
    }
    /**
     * The writer can let go of their own copy, and doing so leaves the
     * copy the other person holds alone.
     */
    public function writerMayLetGoOfTheirOwnCopyTestCase()
    {
        $gone = $this->group_model->deleteMessageCopy(501, self::WRITER,
            self::WRITER_GROUP);
        $this->assertEqual($gone, 1, "the writer lets go of their copy");
        $this->assertEqual($this->countIn(self::WRITER_GROUP), 2,
            "one row fewer is left in their group");
        $this->assertEqual($this->countIn(self::READER_GROUP), 3,
            "and the other person's copy is left where it was");
    }
    /**
     * A message in neither one's own group nor of one's own writing is
     * not theirs to let go of.
     */
    public function anOutsiderMayNotLetGoTestCase()
    {
        $gone = $this->group_model->deleteMessageCopy(501, 9, 90);
        $this->assertEqual($gone, 0,
            "someone with no part in the message lets go of nothing");
        $this->assertEqual($this->countIn(self::WRITER_GROUP), 3,
            "and every row is left where it was");
    }
    /**
     * Letting go of a whole conversation takes every message one person
     * holds in it, keeps the thread they hang from, and leaves the
     * other person's copies untouched.
     */
    public function wholeConversationGoesForOnePersonTestCase()
    {
        $gone = $this->group_model->deleteConversationCopies(self::THREAD,
            self::READER_GROUP);
        $this->assertEqual($gone, 2,
            "both messages that person held are let go");
        $this->assertEqual($this->countIn(self::READER_GROUP), 1,
            "the thread they hang from is kept");
        $this->assertEqual($this->countIn(self::WRITER_GROUP), 3,
            "and the other person's conversation is left whole");
    }
    /**
     * Taking a conversation off one person's list lets go of the row it
     * hung from in their group and leaves the other person's row where
     * it was, so the conversation stays on the other's list.
     */
    public function conversationLeavesOneListOnlyTestCase()
    {
        $gone = $this->group_model->deleteConversationThreadRow(
            self::READER_GROUP, "3-4");
        $this->assertEqual($gone, 1,
            "the row it hung from in that group is let go");
        $this->assertEqual($this->countIn(self::READER_GROUP), 2,
            "the messages under it are left for a separate letting go");
        $this->assertEqual($this->countIn(self::WRITER_GROUP), 3,
            "and the other person's conversation is left whole");
    }
    /**
     * Owned-group counts reflect each owner's rows and no others.
     */
    public function countGroupsOwnedByUserTestCase()
    {
        $this->assertEqual(2,
            (int)$this->group_model->countGroupsOwnedByUser(5),
            "user 5 owns two groups");
        $this->assertEqual(1,
            (int)$this->group_model->countGroupsOwnedByUser(6),
            "user 6 owns one group");
        $this->assertEqual(0,
            (int)$this->group_model->countGroupsOwnedByUser(9),
            "a user owning nothing counts zero");
    }
    /**
     * Member count reflects the seeded memberships of a group.
     */
    public function countGroupUsersTestCase()
    {
        $this->assertEqual(3,
            (int)$this->group_model->countGroupUsers(100),
            "group 100 has three members");
    }
    /**
     * Wiki-page count reflects the pages belonging to a group.
     */
    public function countGroupWikiPagesTestCase()
    {
        $this->assertEqual(2,
            (int)$this->group_model->countGroupWikiPages(100),
            "group 100 has two wiki pages");
        $this->assertEqual(0,
            (int)$this->group_model->countGroupWikiPages(101),
            "group 101 has no wiki pages");
    }
    /**
     * Thread count reflects only the top-level items of a group.
     */
    public function countGroupThreadsTestCase()
    {
        $this->assertEqual(2,
            (int)$this->group_model->countGroupThreads(100),
            "group 100 has two threads, not counting replies");
    }
    /**
     * Checks that a file asked for as a thumb is drawn as the small
     * picture kept beside it, whatever the file is, so a document shows
     * its front page rather than opening in a frame of its own. The
     * picture lives at the file's own address under another word.
     */
    public function thumbOfDocumentDrawsItsPictureTestCase()
    {
        $model = new GroupModel();
        $folders = $model->getGroupPageResourcesFolders(2, 1, "", true);
        $written = $folders[0] . "/notes.pdf";
        file_put_contents($written, "a document stands here");
        $beside = $folders[1] . "/notes.pdf.webp";
        file_put_contents($beside, "a picture stands here");
        $page = "<p>((resource-thumb:notes.pdf|The notes))</p>";
        $drawn = $model->insertResourcesParsePage(2, 1, "en-US", $page,
            "", "group");
        unlink($beside);
        $bare = $model->insertResourcesParsePage(2, 1, "en-US", $page,
            "", "group");
        unlink($written);
        $this->assertTrue(strpos($bare, "wd/thumbs/") === false,
            "a file with no picture beside it does not point at one");
        $this->assertTrue(strpos($drawn, "wd/thumbs/") !== false,
            "a thumb of a document points at the picture beside it");
        $this->assertTrue(strpos($drawn, "<iframe") === false,
            "a thumb of a document is not opened in a frame");
    }
    /**
     * Post count reflects a thread's starting item and its replies.
     */
    public function countThreadPostsTestCase()
    {
        $this->assertEqual(3,
            (int)$this->group_model->countThreadPosts(1),
            "thread 1 holds its start item plus two replies");
        $this->assertEqual(1,
            (int)$this->group_model->countThreadPosts(2),
            "thread 2 holds only its start item");
    }
}
X