SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ tests / WikiIssueTest.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
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\library\wiki\WikiIssue;
use seekquarry\yioop\models\WikiModel;

/**
 * Tests for WikiIssue, which shapes the record of a single tracked issue.
 * Each test starts from a freshly opened issue and checks that opening,
 * assigning, closing, reopening, and re-prioritizing set the right fields
 * and add the right history entry, and that closing with an unknown reason
 * falls back to fixed.
 *
 * @author Chris Pollett
 */
class WikiIssueTest extends UnitTest
{
    /**
     * Nothing to set up; every test builds its own issue record.
     */
    public function setUp()
    {
    }
    /**
     * Nothing to tear down.
     */
    public function tearDown()
    {
    }
    /**
     * A freshly opened issue is open, unassigned, starts at middle urgency
     * because the reporter does not pick one, keeps its reporter, title,
     * branch, and version, and has a single opened history entry.
     */
    public function openTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $this->assertEqual(WikiIssue::STATUS_OPEN, $issue["status"],
            "a new issue is open");
        $this->assertEqual(7, $issue["reporter"],
            "the reporter is remembered");
        $this->assertEqual(WikiIssue::PRIORITY_MEDIUM, $issue["priority"],
            "a new issue starts at middle urgency");
        $this->assertEqual("Login bug", $issue["title"],
            "the title is remembered");
        $this->assertEqual("main", $issue["branch"],
            "the branch is remembered");
        $this->assertEqual("current", $issue["version"],
            "the version is remembered");
        $this->assertEqual(0, $issue["assignee"],
            "a new issue is not assigned to anyone");
        $this->assertEqual(1, count($issue["history"]),
            "a new issue has one history entry");
        $this->assertEqual(WikiIssue::ACTION_OPENED,
            $issue["history"][0]["action"],
            "the one history entry records the opening");
    }
    /**
     * Assigning an issue records the assignee and adds an assigned history
     * entry naming who made the change and who received the issue.
     */
    public function assignTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::assign($issue, 9, 3, 1100);
        $this->assertEqual(9, $issue["assignee"],
            "the issue is now assigned to person nine");
        $last = end($issue["history"]);
        $this->assertEqual(WikiIssue::ACTION_ASSIGNED, $last["action"],
            "the change is recorded as an assignment");
        $this->assertEqual(3, $last["by"],
            "the person who made the change is recorded");
        $this->assertEqual(9, $last["to"],
            "the person the issue went to is recorded");
    }
    /**
     * Closing an issue as fixed marks it closed with that reason and adds a
     * closed history entry.
     */
    public function closeFixedTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
            1200);
        $this->assertEqual(WikiIssue::STATUS_CLOSED, $issue["status"],
            "the issue is now closed");
        $this->assertEqual(WikiIssue::RESOLUTION_FIXED, $issue["resolution"],
            "the issue is recorded as fixed");
        $last = end($issue["history"]);
        $this->assertEqual(WikiIssue::ACTION_CLOSED, $last["action"],
            "the change is recorded as a closing");
    }
    /**
     * Closing an issue as won't-fix keeps that reason.
     */
    public function closeWontFixTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_WONT_FIX, 3,
            1200);
        $this->assertEqual(WikiIssue::RESOLUTION_WONT_FIX,
            $issue["resolution"], "the won't-fix reason is kept");
    }
    /**
     * Closing an issue with an unrecognized reason falls back to fixed
     * rather than storing the bad reason.
     */
    public function closeUnknownReasonTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::close($issue, "banana", 3, 1200);
        $this->assertEqual(WikiIssue::RESOLUTION_FIXED, $issue["resolution"],
            "an unknown closing reason becomes fixed");
    }
    /**
     * Reopening a closed issue makes it open again and clears the reason it
     * had been closed.
     */
    public function reopenTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
            1200);
        $issue = WikiIssue::reopen($issue, 3, 1300);
        $this->assertEqual(WikiIssue::STATUS_OPEN, $issue["status"],
            "the issue is open again");
        $this->assertEqual("", $issue["resolution"],
            "the old closing reason is cleared");
        $last = end($issue["history"]);
        $this->assertEqual(WikiIssue::ACTION_REOPENED, $last["action"],
            "the reopening is recorded");
    }
    /**
     * Changing an issue's priority updates it and records the change.
     */
    public function setPriorityTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::setPriority($issue, 5, 3, 1400);
        $this->assertEqual(5, $issue["priority"],
            "the new priority is stored");
        $last = end($issue["history"]);
        $this->assertEqual(WikiIssue::ACTION_PRIORITY, $last["action"],
            "the priority change is recorded");
    }
    /**
     * Closing an issue as fixed with a commit keeps the commit on the record
     * and in the history entry.
     */
    public function closeWithCommitTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
            1200, "abc123");
        $this->assertEqual("abc123", $issue["fix_commit"],
            "the fixing commit is kept on the record");
        $last = end($issue["history"]);
        $this->assertEqual("abc123", $last["commit"],
            "the fixing commit is kept in the history entry");
    }
    /**
     * The display status reads reported, then assigned, then marked fixed,
     * and reported again once reopened.
     */
    public function displayStatusTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $this->assertEqual(WikiIssue::DISPLAY_REPORTED,
            WikiIssue::displayStatus($issue),
            "a new issue reads as reported");
        $issue = WikiIssue::assign($issue, 9, 3, 1100);
        $this->assertEqual(WikiIssue::DISPLAY_ASSIGNED,
            WikiIssue::displayStatus($issue),
            "an assigned issue reads as assigned");
        $issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
            1200);
        $this->assertEqual(WikiIssue::DISPLAY_FIXED,
            WikiIssue::displayStatus($issue),
            "a fixed issue reads as marked fixed");
        $issue = WikiIssue::reopen($issue, 3, 1300);
        $this->assertEqual(WikiIssue::DISPLAY_ASSIGNED,
            WikiIssue::displayStatus($issue),
            "a reopened issue that still has an owner reads as assigned");
    }
    /**
     * Putting an issue back to reported opens it, clears the assignee, and
     * clears any closing reason.
     */
    public function reportTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $issue = WikiIssue::assign($issue, 9, 3, 1100);
        $issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
            1200);
        $issue = WikiIssue::report($issue, 3, 1300);
        $this->assertEqual(WikiIssue::STATUS_OPEN, $issue["status"],
            "the issue is open again");
        $this->assertEqual(0, $issue["assignee"],
            "the issue is held by no one");
        $this->assertEqual(WikiIssue::DISPLAY_REPORTED,
            WikiIssue::displayStatus($issue),
            "the issue reads as reported");
    }
    /**
     * Checks that the time an issue was reported is read off the opening
     * line of its history, and that a record with no history reads as
     * zero rather than failing.
     */
    public function reportedTimeTestCase()
    {
        $record = WikiIssue::open(5, 1234, "a title", "main", "abc");
        $this->assertEqual(WikiIssue::reportedTime($record), 1234,
            "the reported time is the first history line's time");
        $this->assertEqual(WikiIssue::reportedTime([]), 0,
            "a record with no history reports zero");
    }
    /**
     * Checks that the reporter shown for an issue is the username when an
     * account reported it, and the handle typed in when nobody signed in
     * did. A handle is preferred even if a username is somehow also given,
     * since only an anonymous report carries one.
     */
    public function reporterNameTestCase()
    {
        $record = WikiIssue::open(5, 1000, "a title", "main", "abc");
        $this->assertEqual(WikiIssue::reporterName($record, "alice"),
            "alice", "an account's report shows that account's name");
        $this->assertEqual(WikiIssue::reporterName($record, ""), "",
            "no account and no handle shows nothing");
        $record["handle"] = "sam@example.com";
        $this->assertEqual(WikiIssue::reporterName($record, ""),
            "sam@example.com", "a report with no account shows its handle");
        $this->assertEqual(WikiIssue::reporterName($record, "alice"),
            "sam@example.com", "a handle is preferred to a username");
    }
    /**
     * The user answerable for an issue's status follows the status: the
     * reporter for one merely reported, the assignee for one assigned, and
     * whoever last closed it for one fixed or won't be fixed.
     */
    public function statusUserTestCase()
    {
        $issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
        $this->assertEqual(7, WikiIssue::statusUser($issue),
            "a reported issue points at its reporter");
        $assigned = WikiIssue::assign($issue, 9, 7, 1100);
        $this->assertEqual(9, WikiIssue::statusUser($assigned),
            "an assigned issue points at its assignee");
        $closed = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 12,
            1200);
        $this->assertEqual(12, WikiIssue::statusUser($closed),
            "a closed issue points at whoever closed it");
    }
    /**
     * labelsTestCase checks that setLabels cleans the names, drops empty
     * and repeated ones, notes the change in the history and keeps the
     * labels as the record's last field, and that labelsLast moves them
     * back there after a field is added.
     */
    public function labelsTestCase()
    {
        $record = WikiIssue::open(5, 1000, "Crash", "master", "abc");
        $this->assertEqual(($record["labels"] ?? []), [],
            "a new issue has no labels");
        $record = WikiIssue::setLabels($record, ["bug", " ui  work ",
            "bug", "%\"", "caf\u{e9}"], 7, 1100);
        $this->assertEqual(($record["labels"] ?? []),
            ["bug", "ui work", "caf\u{e9}"],
            "names are cleaned and empty and repeated ones dropped");
        $this->assertEqual(array_key_last($record), "labels",
            "the labels are the last field");
        $this->assertEqual(end($record["history"])["action"],
            WikiIssue::ACTION_LABELS, "the change is in the history");
        $record["description"] = "added after";
        $this->assertEqual(array_key_last(WikiIssue::labelsLast($record)),
            "labels", "labelsLast moves the labels to the end");
    }
    /**
     * labelPatternFindsOnlyIssuesWithTheLabelTestCase stores issue records
     * the way WikiModel does, with quotation marks written as entities, in
     * a database held in memory, and checks that the label filter's LIKE
     * pattern finds just the issues carrying the label: not a longer label
     * that starts the same, and not the word in a title or the history.
     */
    public function labelPatternFindsOnlyIssuesWithTheLabelTestCase()
    {
        $db = new \SQLite3(":memory:");
        $db->exec("CREATE TABLE GROUP_PAGE (ID INTEGER, PAGE TEXT)");
        $records = [
            1 => ["bug"],
            2 => ["bug fix", "ui"],
            3 => [],
            4 => ["ui", "caf\u{e9}"],
        ];
        foreach ($records as $id => $labels) {
            $record = WikiIssue::open(5, 1000, "bug in caf\u{e9}", "master",
                "abc");
            $record = WikiIssue::setLabels($record, ["bug"], 7, 1100);
            $record = WikiIssue::setLabels($record, $labels, 7, 1200);
            $page = str_replace('"', "&quot;", json_encode(
                WikiIssue::labelsLast($record)));
            $statement = $db->prepare("INSERT INTO GROUP_PAGE VALUES (?, ?)");
            $statement->bindValue(1, $id);
            $statement->bindValue(2, $page);
            $statement->execute();
        }
        $found = function ($label) use ($db) {
            $statement = $db->prepare("SELECT ID FROM GROUP_PAGE WHERE " .
                "PAGE LIKE ? ORDER BY ID");
            $statement->bindValue(1, WikiModel::gitIssueLabelPattern($label));
            $result = $statement->execute();
            $ids = [];
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
                $ids[] = $row["ID"];
            }
            return $ids;
        };
        $this->assertEqual($found("bug"), [1],
            "only the issue labeled bug, not bug fix, a title or history");
        $this->assertEqual($found("ui"), [2, 4],
            "a label anywhere in the list");
        $this->assertEqual($found("caf\u{e9}"), [4],
            "a label with a letter outside ASCII");
        $db->close();
    }
    /**
     * issuePageMarksHideLabelListTestCase checks that the mark naming a
     * repository's label list page, as in yioop$*, is among the marks
     * WikiModel uses to leave issue pages out of a group's page list and to
     * delete them with the repository page. The label list page showed in
     * the page list before it was added.
     */
    public function issuePageMarksHideLabelListTestCase()
    {
        $this->assertTrue(in_array(C\GIT_ISSUE_LABEL_SEPARATOR,
            WikiModel::GIT_ISSUE_PAGE_MARKS),
            "the label list page is left out of the page list");
        $this->assertEqual(count(WikiModel::GIT_ISSUE_PAGE_MARKS), 4,
            "issues, waiting issues, bans and the label list");
    }
}