Ctrl K
ComunidadGuías…

tests/Unit/XmlParserTest.php

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php

declare(strict_types=1);

/**
 * Copyright 2026 codaipro — Lucky Yaduvanshi (https://luckyyaduvanshi.in)
 * Original source: https://github.com/Luckyyaduvanshiofficial/php-s3
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace PhpS3\Tests\Unit;

use PhpS3\S3\Exception\S3Exception;
use PhpS3\S3\Xml\XmlParser;
use PHPUnit\Framework\TestCase;

final class XmlParserTest extends TestCase
{
    public function testDeleteRequestParsesKeysAndQuietFlag(): void
    {
        $body = '<?xml version="1.0" encoding="UTF-8"?>'
            . '<Delete><Quiet>true</Quiet>'
            . '<Object><Key>a.txt</Key></Object>'
            . '<Object><Key>dir/b c.txt</Key><VersionId>v1</VersionId></Object>'
            . '</Delete>';

        $parsed = XmlParser::deleteRequest($body);

        self::assertTrue($parsed['quiet']);
        self::assertSame(['a.txt', 'dir/b c.txt'], $parsed['objects']);
    }

    public function testDeleteRequestDefaultsToVerbose(): void
    {
        $parsed = XmlParser::deleteRequest('<Delete><Object><Key>x</Key></Object></Delete>');

        self::assertFalse($parsed['quiet']);
        self::assertSame(['x'], $parsed['objects']);
    }

    public function testDeleteRequestEmptyBodyThrowsMalformedXml(): void
    {
        $this->expectException(S3Exception::class);
        $this->expectExceptionMessageMatches('/MalformedXML/');

        XmlParser::deleteRequest('   ');
    }

    public function testDeleteRequestGarbageThrowsMalformedXml(): void
    {
        $this->expectException(S3Exception::class);

        XmlParser::deleteRequest('<Delete><Object></Delete>');
    }

    public function testDeleteRequestWrongRootElementThrows(): void
    {
        $this->expectException(S3Exception::class);
        $this->expectExceptionMessageMatches('/expected <Delete>/');

        XmlParser::deleteRequest('<CompleteMultipartUpload/>');
    }

    public function testDeleteRequestRejectsDtdEntityExpansion(): void
    {
        $body = '<?xml version="1.0"?><!DOCTYPE Delete [<!ENTITY x SYSTEM "file:///etc/passwd">]>'
            . '<Delete><Object><Key>&x;</Key></Object></Delete>';

        // DOCTYPE is rejected outright: no entity resolution can ever run,
        // and no parser quirks are reachable.
        $this->expectException(S3Exception::class);
        $this->expectExceptionMessageMatches('/DOCTYPE/');

        XmlParser::deleteRequest($body);
    }

    public function testReadBodyReadsACompleteStream(): void
    {
        $stream = fopen('php://temp', 'r+');
        fwrite($stream, '<Delete></Delete>');
        rewind($stream);

        try {
            self::assertSame('<Delete></Delete>', XmlParser::readBody($stream));
        } finally {
            fclose($stream);
        }
    }

    public function testReadBodyRejectsBodiesOverTheCap(): void
    {
        $stream = fopen('php://temp', 'r+');
        fwrite($stream, str_repeat('a', 8 * 1024 * 1024 + 1));
        rewind($stream);

        try {
            $this->expectException(S3Exception::class);
            $this->expectExceptionMessageMatches('/too large/');
            XmlParser::readBody($stream);
        } finally {
            fclose($stream);
        }
    }

    public function testCompleteRequestParsesPartsAndUnquotesEtags(): void
    {
        $body = '<CompleteMultipartUpload>'
            . '<Part><PartNumber>1</PartNumber><ETag>"abc123"</ETag></Part>'
            . '<Part><PartNumber>2</PartNumber><ETag>def456</ETag></Part>'
            . '</CompleteMultipartUpload>';

        self::assertSame(
            [
                ['partNumber' => 1, 'etag' => 'abc123'],
                ['partNumber' => 2, 'etag' => 'def456'],
            ],
            XmlParser::completeRequest($body),
        );
    }

    public function testCompleteRequestRejectsOutOfRangePartNumber(): void
    {
        $this->expectException(S3Exception::class);
        $this->expectExceptionMessageMatches('/PartNumber/');

        XmlParser::completeRequest(
            '<CompleteMultipartUpload><Part><PartNumber>10001</PartNumber><ETag>x</ETag></Part></CompleteMultipartUpload>',
        );
    }

    public function testCompleteRequestRejectsMissingEtag(): void
    {
        $this->expectException(S3Exception::class);

        XmlParser::completeRequest(
            '<CompleteMultipartUpload><Part><PartNumber>1</PartNumber></Part></CompleteMultipartUpload>',
        );
    }
}