<?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");
}
}