/ tests / ImapResponseParserTest.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\mail\ImapResponseParser;
use seekquarry\yioop\library\UnitTest;

/**
 * Unit tests for ImapResponseParser, covering both the CAPABILITY
 * tokenizer and the SORT sequence-number parser. The cases here
 * are the shapes the Mail activity depends on: SORT being absent
 * from a server's advertised capabilities so column-sort headers
 * stay non-clickable, a SORT response with a handful of sequence
 * numbers in the order the server chose, and a few empty-or-odd
 * shapes where the parsers must degrade quietly rather than blow
 * up.
 *
 * @author Chris Pollett
 */
class ImapResponseParserTest extends UnitTest
{
    /**
     * No setup state is needed; both parsers are pure and static.
     */
    public function setUp()
    {
    }
    /**
     * No setup state is needed; nothing to tear down.
     */
    public function tearDown()
    {
    }
    /**
     * Gmail-shaped CAPABILITY line that advertises SORT among many
     * other tokens. The parser must find SORT regardless of where
     * in the token list it appears.
     */
    public function capabilityFoundTestCase()
    {
        $untagged = ['* CAPABILITY IMAP4rev1 UNSELECT IDLE ' .
            'NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 ' .
            'UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ' .
            'ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS ' .
            'LITERAL- SPECIAL-USE APPENDLIMIT=35651584 SORT'];
        $this->assertTrue(ImapResponseParser::hasCapability(
            $untagged, 'SORT'),
            "SORT detected at end of long capability list");
    }
    /**
     * A CAPABILITY line that omits SORT must return false. This
     * is the path the Mail activity falls back through on smaller
     * servers, leaving Subject and From headers non-clickable.
     */
    public function capabilityMissingTestCase()
    {
        $untagged = ['* CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED'];
        $this->assertFalse(ImapResponseParser::hasCapability(
            $untagged, 'SORT'),
            "SORT absent from advertised capabilities");
    }
    /**
     * Capability tokens are case-insensitive per RFC 3501; both
     * the line and the lookup must be compared without regard to
     * case. The test uses an unusual mixed casing to be sure.
     */
    public function capabilityCaseInsensitiveTestCase()
    {
        $untagged = ['* CAPABILITY IMAP4rev1 sort'];
        $this->assertTrue(ImapResponseParser::hasCapability(
            $untagged, 'Sort'),
            "Lowercase sort token matches mixed-case lookup");
    }
    /**
     * When the response contains no CAPABILITY line at all, the
     * parser returns false rather than treating other untagged
     * content as if it were a capability list.
     */
    public function capabilityNoCapabilityLineTestCase()
    {
        $untagged = ['* 42 EXISTS', '* 0 RECENT'];
        $this->assertFalse(ImapResponseParser::hasCapability(
            $untagged, 'SORT'),
            "No CAPABILITY line means the capability is absent");
    }
    /**
     * A standard SORT response with a handful of sequence numbers
     * in non-numeric order: the parser must preserve the order the
     * server gave so the caller can paginate the list as-is.
     */
    public function sortBasicTestCase()
    {
        $untagged = ['* SORT 12 4 89 7'];
        $this->assertEqual([12, 4, 89, 7],
            ImapResponseParser::parseSort($untagged),
            "Sequence numbers preserved in server-given order");
    }
    /**
     * The server may match no messages and return a SORT line
     * with no numbers. The parser yields an empty array rather
     * than failing.
     */
    public function sortEmptyResultTestCase()
    {
        $untagged = ['* SORT'];
        $this->assertEqual([],
            ImapResponseParser::parseSort($untagged),
            "Empty SORT line parses to an empty list");
    }
    /**
     * SORT may be followed by extra whitespace between the keyword
     * and the first sequence number, or by trailing whitespace.
     * The parser must split flexibly and drop empties.
     */
    public function sortWhitespaceVariantsTestCase()
    {
        $untagged = ['* SORT   1   2   3   '];
        $this->assertEqual([1, 2, 3],
            ImapResponseParser::parseSort($untagged),
            "Extra whitespace between tokens is tolerated");
    }
    /**
     * When the response contains untagged noise lines before the
     * SORT line, the parser must skip them and read the SORT line
     * out of the middle of the list.
     */
    public function sortFindsLineAmongOthersTestCase()
    {
        $untagged = ['* OK something', '* SORT 5 9 1',
            '* BYE done'];
        $this->assertEqual([5, 9, 1],
            ImapResponseParser::parseSort($untagged),
            "SORT line found regardless of position");
    }
    /**
     * A response with no SORT line at all comes back as an empty
     * list, matching the empty-result shape.
     */
    public function sortNoSortLineTestCase()
    {
        $untagged = ['* 42 EXISTS', '* 0 RECENT'];
        $this->assertEqual([],
            ImapResponseParser::parseSort($untagged),
            "Missing SORT line yields the empty list");
    }
    /**
     * A standard SEARCH response with a handful of matching
     * sequence numbers: the parser preserves the order the server
     * gave (typically ascending, but the spec does not require it).
     */
    public function searchBasicTestCase()
    {
        $untagged = ['* SEARCH 1 5 12 33'];
        $this->assertEqual([1, 5, 12, 33],
            ImapResponseParser::parseSearch($untagged),
            "Matching sequence numbers preserved in given order");
    }
    /**
     * A SEARCH that finds nothing emits an empty SEARCH line; the
     * parser yields an empty array rather than failing.
     */
    public function searchEmptyResultTestCase()
    {
        $untagged = ['* SEARCH'];
        $this->assertEqual([],
            ImapResponseParser::parseSearch($untagged),
            "Empty SEARCH line parses to an empty list");
    }
    /**
     * Untagged noise lines before the SEARCH line are skipped;
     * the parser finds the SEARCH line wherever it sits.
     */
    public function searchFindsLineAmongOthersTestCase()
    {
        $untagged = ['* OK something', '* SEARCH 7 11',
            '* BYE done'];
        $this->assertEqual([7, 11],
            ImapResponseParser::parseSearch($untagged),
            "SEARCH line found regardless of position");
    }
    /**
     * A response with no SEARCH line at all yields the empty
     * list rather than picking up data from unrelated untagged
     * lines.
     */
    public function searchNoSearchLineTestCase()
    {
        $untagged = ['* 42 EXISTS', '* 0 RECENT'];
        $this->assertEqual([],
            ImapResponseParser::parseSearch($untagged),
            "Missing SEARCH line yields the empty list");
    }
}
X