/ tests / DiscussionImportTest.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\library\DiscussionImport;
use seekquarry\yioop\library\UnitTest;

/**
 * Unit tests for DiscussionImport, the pure helper logic behind importing a
 * MantisBT bug tracker into a Yioop issue tracker. Each test checks one
 * piece on its own, with no database: reading a dump's INSERT rows, judging
 * a username, building a byline and an issue body, decoding a stored
 * attachment, and writing the wiki references for a post's attachments.
 *
 * @author Chris Pollett
 */
class DiscussionImportTest extends UnitTest
{
    /**
     * These tests need nothing set up beforehand.
     */
    public function setUp()
    {
    }
    /**
     * These tests leave nothing behind to clean up.
     */
    public function tearDown()
    {
    }
    /**
     * The INSERT reader keeps a quoted comma inside its field, turns a
     * doubled quote into a single one, reads an unquoted NULL as a real
     * null, and returns nothing for a table the dump does not mention.
     */
    public function parseInsertsTestCase()
    {
        $sql = "INSERT INTO `t` VALUES (1,'it''s','a,b'),(2,NULL,'x');";
        $rows = DiscussionImport::parseInserts($sql, "t");
        $this->assertEqual(2, count($rows), "two rows are read");
        $this->assertEqual("it's", $rows[0][1],
            "a doubled quote becomes a single one");
        $this->assertEqual("a,b", $rows[0][2],
            "a comma inside quotes stays in the field");
        $this->assertTrue($rows[1][1] === null,
            "an unquoted NULL is read as null");
        $this->assertEqual(0, count(DiscussionImport::parseInserts($sql, "u")),
            "a table not in the dump yields no rows");
    }
    /**
     * A backslash-n in a quoted field is read back as a real newline, so
     * multi-line descriptions survive the dump.
     */
    public function parseInsertsEscapeTestCase()
    {
        $sql = "INSERT INTO `t` VALUES ('line1" . "\\n" . "line2');";
        $rows = DiscussionImport::parseInserts($sql, "t");
        $this->assertEqual("line1\nline2", $rows[0][0],
            "an escaped newline is decoded");
    }
    /**
     * A username can be reused when it is non-empty, short enough, and made
     * only of letters, digits, dots, dashes, and underscores; a blank name,
     * one with a space, or an over-long one cannot.
     */
    public function usernameCompatibleTestCase()
    {
        $this->assertTrue(DiscussionImport::usernameCompatible("alice"),
            "a plain name can be reused");
        $this->assertTrue(DiscussionImport::usernameCompatible("a.b-c_d"),
            "dots, dashes, and underscores are allowed");
        $this->assertFalse(DiscussionImport::usernameCompatible(""),
            "a blank name cannot be reused");
        $this->assertFalse(DiscussionImport::usernameCompatible("has space"),
            "a name with a space cannot be reused");
        $this->assertFalse(
            DiscussionImport::usernameCompatible(str_repeat("a", 100)),
            "an over-long name cannot be reused");
    }
    /**
     * A fallback byline prefers the real name with the email in angle
     * brackets, falls back to the username when there is no real name, and
     * is empty when nothing at all is known.
     */
    public function fallbackBylineTestCase()
    {
        $this->assertEqual("Alice A <a@x.com>",
            DiscussionImport::fallbackByline(["realname" => "Alice A",
            "username" => "alice", "email" => "a@x.com"]),
            "real name and email are shown together");
        $this->assertEqual("alice",
            DiscussionImport::fallbackByline(["realname" => "",
            "username" => "alice", "email" => ""]),
            "the username stands in when nothing else is known");
        $this->assertEqual("",
            DiscussionImport::fallbackByline(["realname" => "",
            "username" => "", "email" => ""]),
            "nothing known gives an empty byline");
    }
    /**
     * The issue body keeps the description, steps, and extra information,
     * adds the original-reporter byline only for a bot-folded user, and
     * never adds the old source-metadata footer.
     */
    public function issueBodyTestCase()
    {
        $body = DiscussionImport::issueBody(["the bug", "do x", "a note"], "");
        $this->assertTrue(strpos($body, "the bug") !== false,
            "the description is kept");
        $this->assertTrue(strpos($body, "Steps to reproduce") !== false,
            "the steps are kept");
        $this->assertTrue(strpos($body, "Additional information") !== false,
            "the extra information is kept");
        $this->assertTrue(strpos($body, "Imported from MantisBT") === false,
            "the source-metadata footer is not added");
        $with_note = DiscussionImport::issueBody(["b", "", ""], "Grace <g@x>");
        $this->assertTrue(
            strpos($with_note, "Originally reported by Grace <g@x>") !==
            false, "a fallback reporter is credited");
    }
    /**
     * Attachment bytes come through unchanged when stored plainly, are
     * decoded when stored as a hex literal, and are empty when the dump
     * holds none.
     */
    public function fileContentTestCase()
    {
        $this->assertEqual("hi", DiscussionImport::fileContent("hi"),
            "plain bytes are kept as is");
        $this->assertEqual("Hi", DiscussionImport::fileContent("0x4869"),
            "a hex literal is decoded");
        $this->assertEqual("", DiscussionImport::fileContent(""),
            "an empty value stays empty");
        $this->assertEqual("", DiscussionImport::fileContent(null),
            "a null value becomes empty");
    }
    /**
     * The wiki references name one resource per attachment that has bytes,
     * and skip an attachment the dump had no bytes for.
     */
    public function fileReferencesTestCase()
    {
        $refs = DiscussionImport::fileReferences([
            ["filename" => "a.png", "content" => "x"],
            ["filename" => "b.txt", "content" => ""],
            ["filename" => "c.log", "content" => "y"]]);
        $this->assertTrue(strpos($refs, "((resource:a.png|a.png))") !== false,
            "a file with bytes is referenced");
        $this->assertTrue(strpos($refs, "b.txt") === false,
            "a file with no bytes is not referenced");
        $this->assertTrue(strpos($refs, "((resource:c.log|c.log))") !== false,
            "another file with bytes is referenced");
    }
}
X