SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ tests / V10ResourceFolderMigrationTest.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 as L;
use seekquarry\yioop\library\V10ResourceFolderMigration;
use seekquarry\yioop\library\ResourceFolders;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\models\FeedModel;
use seekquarry\yioop\models\Model;
use seekquarry\yioop\models\ProfileModel;
use seekquarry\yioop\models\datasources\Sqlite3Manager;

/**
 * V10ResourceFolderMigrationTest stands up a site's database and an
 * app/resources in the pre-version 10 layout, with folders named by
 * hashes of ids and an AUTH_KEY, runs the version 10 migration, and
 * checks where everything went and that a second run does nothing.
 *
 * @author Chris Pollett
 */
class V10ResourceFolderMigrationTest extends UnitTest
{
    /**
     * Folder under the test files the throwaway database and resources
     * folder are made in.
     */
    const TEST_DIR = '/test_files/resource_folder_migration';
    /**
     * PRE_V10_KEY is the AUTH_KEY the pre-version 10 folder names are made
     * with.
     */
    const PRE_V10_KEY = "a site key";
    /**
     * The time of the message user writer sent.
     */
    const MESSAGE_TIME = 1700000000;
    /**
     * A throwaway Sqlite3 database built from ProfileModel definitions.
     * @var object
     */
    public $db;
    /**
     * Folder the database and resources folder are made in.
     * @var string
     */
    public $where;
    /**
     * The stand-in for app/resources.
     * @var string
     */
    public $resources;
    /**
     * Builds the throwaway database: users writer (5) and reader (6);
     * Group 100, owned by writer, with page Page 1 in en-US; writer's
     * personal group Personal$5; a group Personal$6 that writer made
     * before reader had a personal group; a group named reader$Personal
     * that writer made; a message in Personal$5 and its copy in
     * Personal$6; a recommendation of a file of Page 1; and a host with
     * its own appearance.
     */
    public function setUp()
    {
        $this->where = __DIR__ . self::TEST_DIR;
        if (!file_exists($this->where)) {
            mkdir($this->where, 0777, true);
        }
        $this->resources = $this->where . "/resources" . uniqid();
        mkdir($this->resources, 0777, true);
        $this->db = new Sqlite3Manager();
        $this->db->connect("", "", "", $this->where . "/migration" .
            uniqid() . ".db");
        $profile = new ProfileModel(C\DB_NAME, false);
        $profile->initializeSql($this->db, ["DBMS" => "Sqlite3",
            "DB_HOST" => ""]);
        foreach (['USERS', 'SOCIAL_GROUPS', 'GROUP_PAGE', 'GROUP_ITEM',
            'GROUP_RESOURCE_RECOMMENDATION', 'DOMAIN_APPEARANCE'] as $table) {
            $this->db->execute($profile->create_statements[$table]);
        }
        foreach ([[5, "Writer"], [6, "reader"]] as $user) {
            $this->db->execute("INSERT INTO USERS (USER_ID, USER_NAME) " .
                "VALUES (?, ?)", $user);
        }
        foreach ([[100, "Group 100"], [60, "Personal\$5"],
            [61, "Personal\$6"], [62, "reader\$Personal"]] as $group) {
            $this->db->execute("INSERT INTO SOCIAL_GROUPS (GROUP_ID, " .
                "GROUP_NAME, OWNER_ID) VALUES (?, ?, 5)", $group);
        }
        $this->db->execute("INSERT INTO GROUP_PAGE (ID, GROUP_ID, TITLE, " .
            "LOCALE_TAG) VALUES (1, 100, 'Page 1', 'en-US')");
        foreach ([[501, 60], [502, 61]] as $item) {
            $this->db->execute("INSERT INTO GROUP_ITEM (ID, PARENT_ID, " .
                "GROUP_ID, USER_ID, TITLE, DESCRIPTION, PUBDATE) VALUES " .
                "(?, ?, ?, 5, 'hi', 'hi', ?)", [$item[0], $item[0],
                $item[1], self::MESSAGE_TIME]);
        }
        $this->db->execute("INSERT INTO GROUP_RESOURCE_RECOMMENDATION " .
            "(USER_ID, GROUP_ID, PAGE_ID, RESOURCE_PATH, SCORE) VALUES " .
            "(6, 100, 1, ?, 1)", [$this->preV10Path("group1001") . "/a.txt"]);
        $this->db->execute("INSERT INTO DOMAIN_APPEARANCE (DOMAIN, " .
            "APPEARANCE) VALUES ('www.example.com', '')");
    }
    /**
     * Takes down the throwaway database and folders.
     */
    public function tearDown()
    {
        if ($this->db) {
            $this->db->disconnect();
        }
        if ($this->where && file_exists($this->where)) {
            $model = new Model();
            $model->db->unlinkRecursive($this->where);
        }
    }
    /**
     * preV10Path gives the path the pre-version 10 layout gave the folder
     * of a hash of $ids and PRE_V10_KEY, with t before the name for a
     * thumbnail folder.
     *
     * @param string $ids what was hashed with the key, such as group1001
     * @param string $before "t" for a thumbnail folder, else ""
     * @return string the pre-version 10 folder's path
     */
    private function preV10Path($ids, $before = "")
    {
        $hash = L\crawlHash($ids . self::PRE_V10_KEY);
        return $this->resources . "/" . substr($hash, 0, 3) .
            "/$before$hash";
    }
    /**
     * makePreV10Folder makes a pre-version 10 folder holding one file.
     *
     * @param string $path the folder
     * @param string $file_name the file to put in it
     */
    private function makePreV10Folder($path, $file_name)
    {
        mkdir($path, 0777, true);
        file_put_contents("$path/$file_name", $file_name);
    }
    /**
     * preV10LayoutMovesToNamedFoldersTestCase lays out the pre-version 10
     * folders of
     * Page 1 and its thumbnails, writer's message and its copy, a link as
     * linkResourceFolders made it, writer's icon, the feed pictures, the
     * pre-version 10 thumbnails of the path Page 1's redirect.txt names, a
     * folder no row names, a site logo and the host's folder, runs the
     * migration and checks: personal groups are renamed by user name, the
     * group writer made before reader had one becomes an ordinary group,
     * as does the group named reader$Personal; each folder is in its new
     * place, Page 1 keeps its own thumbnails and those of its redirect are
     * set aside, the copy's link points there by a relative path, the folder
     * nothing names is under unmigrated, the logo is in the site folder,
     * the host's folder in domains, and the stored recommendation path is
     * rewritten. A second run finds nothing to do.
     */
    public function preV10LayoutMovesToNamedFoldersTestCase()
    {
        $this->makePreV10Folder($this->preV10Path("group1001"), "a.txt");
        $this->makePreV10Folder($this->preV10Path("group1001", "t"),
            "a.txt.webp");
        /* Page 1 names a shared folder in its redirect.txt; the
           pre-version 10 thumbnails of that path must not take the place
           of Page 1's own */
        file_put_contents($this->preV10Path("group1001") . "/redirect.txt",
            "/elsewhere/shared");
        $shared_hash = L\crawlHash("/elsewhere/shared");
        $this->makePreV10Folder($this->resources . "/" .
            substr($shared_hash, 0, 3) . "/t$shared_hash", "shared.webp");
        $this->makePreV10Folder($this->preV10Path("group60post501"), "m.txt");
        $copy = $this->preV10Path("group61post502");
        mkdir(dirname($copy), 0777, true);
        symlink($this->preV10Path("group60post501"), $copy);
        $this->makePreV10Folder($this->preV10Path("user5"), "user_icon.webp");
        $this->makePreV10Folder($this->preV10Path("group" . C\PUBLIC_GROUP_ID,
            "t") . "/2026-10-01", "p.jpg");
        $this->makePreV10Folder($this->preV10Path("group999"), "lost.txt");
        file_put_contents($this->resources . "/logo.png", "logo");
        file_put_contents($this->resources . "/recommendation.txt",
            $this->preV10Path("group1001") . "\n");
        $this->makePreV10Folder($this->resources . "/www.example.com",
            "logo.png");
        $migration = new V10ResourceFolderMigration($this->db,
            $this->resources, self::PRE_V10_KEY);
        $migration->run();
        $names = [];
        $result = $this->db->execute("SELECT GROUP_ID, GROUP_NAME FROM " .
            "SOCIAL_GROUPS ORDER BY GROUP_ID");
        while ($row = $this->db->fetchArray($result)) {
            $names[$row['GROUP_ID']] = $row['GROUP_NAME'];
        }
        $this->assertEqual($names, [60 => "writer\$Personal",
            61 => "Personal_6", 62 => "reader_Personal",
            100 => "Group 100"], "personal groups are named by user name " .
            "and groups that took such names become ordinary");
        $page = ResourceFolders::wikiPagePath("Group 100", "en-US",
            "Page 1", $this->resources);
        $message = ResourceFolders::postPath("writer\$Personal", "writer",
            self::MESSAGE_TIME, $this->resources);
        $copy_page = ResourceFolders::postPath("Personal_6", "writer",
            self::MESSAGE_TIME, $this->resources) . "/page";
        $this->assertEqual([is_file("$page/page/a.txt"),
            is_file("$page/thumbs/a.txt.webp"),
            is_file("$message/page/m.txt"),
            is_link($copy_page) && is_file("$copy_page/m.txt"),
            str_starts_with((string)readlink($copy_page), "../"),
            is_file(ResourceFolders::userPath("writer", $this->resources) .
            "/user_icon.webp"), is_file(ResourceFolders::feedsPath(
            $this->resources) . "/2026-10-01/p.jpg")],
            array_fill(0, 7, true), "pages, thumbnails, messages, a " .
            "relative link, an icon and the feed pictures are moved");
        $this->assertEqual([is_file("$page/page/redirect.txt"),
            is_file("$page/thumbs/shared.webp"), is_file($this->resources .
            "/unmigrated/" . substr($shared_hash, 0, 3) .
            "/t$shared_hash/shared.webp")], [true, false, true],
            "a page keeps its own thumbnails over those of its redirect");
        $this->assertEqual([is_file($this->resources . "/unmigrated/" .
            basename(dirname($this->preV10Path("group999"))) . "/" .
            basename($this->preV10Path("group999")) . "/lost.txt"),
            is_file($this->resources . "/site/logo.png"),
            is_file($this->resources . "/domains/www.example.com/logo.png"),
            is_dir(dirname($this->preV10Path("group1001")))],
            [true, true, true, false], "a folder nothing names is kept " .
            "aside, site and host files move, and pre-version 10 folders " .
            "are gone");
        $result = $this->db->execute("SELECT RESOURCE_PATH FROM " .
            "GROUP_RESOURCE_RECOMMENDATION");
        $row = $this->db->fetchArray($result);
        $this->assertEqual([$row['RESOURCE_PATH'], trim(file_get_contents(
            $this->resources . "/recommendation.txt"))],
            ["$page/page/a.txt", "$page/page"],
            "stored paths name the new folders");
        $again = (new V10ResourceFolderMigration($this->db, $this->resources,
            self::PRE_V10_KEY))->run();
        unset($again["seconds"]);
        $this->assertEqual(array_sum($again), 0,
            "a second run finds nothing to do");
        unlink($copy_page);
    }
    /**
     * prefixFolderHoldingOnlyFinderFilesGoesTestCase checks the three-letter
     * prefix folders the migration leaves. One holds only the .DS_Store
     * macOS's Finder writes, and one only a Thumbs.db, as Windows Explorer
     * writes; both are deleted, though no hashed folder was ever in them.
     * One holds a file whose name is no pre-version 10 hash; it is kept,
     * and the report names it and that file.
     */
    public function prefixFolderHoldingOnlyFinderFilesGoesTestCase()
    {
        mkdir($this->resources . "/UuB");
        file_put_contents($this->resources . "/UuB/.DS_Store", "finder");
        mkdir($this->resources . "/Wq7");
        file_put_contents($this->resources . "/Wq7/Thumbs.db", "explorer");
        mkdir($this->resources . "/Abc");
        file_put_contents($this->resources . "/Abc/notes.txt", "notes");
        $lines = [];
        $counts = (new V10ResourceFolderMigration($this->db,
            $this->resources, self::PRE_V10_KEY,
            function ($line) use (&$lines) {
                $lines[] = $line;
            }))->run();
        $kept_line = $this->resources . "/Abc, which holds notes.txt";
        $this->assertEqual([is_dir($this->resources . "/UuB"),
            is_dir($this->resources . "/Wq7"),
            is_file($this->resources . "/Abc/notes.txt"),
            $counts["prefix folders kept"] ?? 0,
            in_array("kept " . $kept_line, $lines)],
            [false, false, true, 1, true], "prefix folders holding only " .
            "Finder or Explorer files go, and one kept is reported");
    }
    /**
     * oldPersonalPrefixIsOnlyInTheMigrationTestCase checks that the
     * pre-version 10 personal group prefix is no longer a setting in
     * Config.php. The migration that renames old personal groups keeps it
     * as OLD_PERSONAL_GROUP_PREFIX, and the clipboard thread keeps the
     * title its existing threads already carry.
     */
    public function oldPersonalPrefixIsOnlyInTheMigrationTestCase()
    {
        $this->assertFalse(defined(C\NS_CONFIGS . "PERSONAL_GROUP_PREFIX"),
            "Config.php does not define PERSONAL_GROUP_PREFIX");
        $this->assertEqual(
            V10ResourceFolderMigration::OLD_PERSONAL_GROUP_PREFIX,
            'Personal$', "the migration knows the old personal group names");
        $this->assertEqual(FeedModel::CLIPBOARD_TITLE_PREFIX,
            'Personal$_clipboard_', "clipboard threads keep their title");
    }
}