Ctrl K
ComunidadGuías…

tests/Module/System/InstallationHelperTest.php

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

Archivo original ↗Preguntar a KiBot
<?php

declare(strict_types=1);

/**
 * vim:set softtabstop=4 shiftwidth=4 expandtab:
 *
 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
 * Copyright Ampache.org, 2001-2026
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Ampache\Module\System;

use Ampache\Config\AmpConfig;
use Ampache\MockeryTestCase;
use Ampache\Module\Playback\Stream;
use Ampache\Module\System\Update\UpdaterInterface;
use Ampache\Repository\UpdateInfoRepositoryInterface;
use Override;
use PHPUnit\Framework\Attributes\DataProvider;

class InstallationHelperTest extends MockeryTestCase
{
    private string $configFile = '';
    private string $file       = '';
    private InstallationHelper $subject;

    /**
     * @return array<string, array{array<string, string>, string}>
     */
    public static function sendFullStreamProvider(): array
    {
        return [
            'old default from before version 101' => [['config_version' => '100', 'send_full_stream' => 'webplayer'], ';send_full_stream = "webplayer"'],
            'other value from before version 101' => [['config_version' => '100', 'send_full_stream' => 'api'], 'send_full_stream = "api"'],
            'webplayer chosen from version 101' => [['config_version' => '101', 'send_full_stream' => 'webplayer'], 'send_full_stream = "webplayer"'],
        ];
    }

    public function testAnAlreadyPrefixedTargetIsNotPrefixedTwice(): void
    {
        $rules = 'RewriteRule ^image\.php$ /ampache/image.php [R=302,L]';

        self::assertSame($rules, $this->subject->install_check_rewrite_rules($this->writeRules($rules), '/ampache', true));
    }

    public function testFindConfigSyntaxIssueIgnoresAProperlyEscapedQuote(): void
    {
        $file = $this->writeConfig(implode("\n", [
            'a = "b"',
            'foo = "va\"lue"',
            '; a comment carrying an "unbalanced quote is never the value itself',
        ]));

        self::assertNull($this->subject->findConfigSyntaxIssue($file));
    }

    /**
     * The offending line is the one PHP's own parser never reports: an unclosed quote makes it keep scanning
     * and blame whatever later line it finally trips on, many lines further down the file.
     */
    public function testFindConfigSyntaxIssueReportsTheUnclosedLineNotTheOneParseIniFileBlames(): void
    {
        $file = $this->writeConfig(implode("\n", [
            'a = "b"',
            'foo = "va"lue"',
            'c = "d"',
            '; DEFAULT: "false"',
        ]));

        $issue = $this->subject->findConfigSyntaxIssue($file);

        self::assertNotNull($issue);
        self::assertSame(2, $issue['line']);
        self::assertSame('foo = "va"lue"', $issue['content']);
    }

    public function testFindConfigSyntaxIssueReturnsNullForANonexistentFile(): void
    {
        self::assertNull($this->subject->findConfigSyntaxIssue(sys_get_temp_dir() . '/nonexistent-ampache.cfg.php'));
    }

    /**
     * A '-' substitution means the rule only sets flags. Prefixing it produces '/ampache-', which is
     * a path no request matches, so the rule silently stops applying on a subdirectory install.
     */
    public function testFlagOnlyRulesKeepTheirSubstitution(): void
    {
        $rules = implode("\n", [
            'RewriteRule ^(bin|config|vendor)(/|$) - [F,L]',
            'RewriteRule (^|/)\. - [F,L]',
            'RewriteRule (.*) - [R=400,L]',
        ]);

        self::assertSame(
            $rules,
            $this->subject->install_check_rewrite_rules($this->writeRules($rules), '/ampache', true)
        );
    }

    /**
     * @param array<string, string> $current
     */
    #[DataProvider('sendFullStreamProvider')]
    public function testGenerateConfigOnlyDropsTheRetiredSendFullStreamDefault(array $current, string $expected): void
    {
        $lines = array_values(array_filter(
            explode("\n", $this->subject->generate_config($current)),
            static fn(string $line): bool => str_contains($line, 'send_full_stream =')
        ));

        self::assertSame([$expected], $lines);
    }

    /**
     * The 'transcode' preference (the opt-in switch for transcoding) defaults to 'default' for every user,
     * not 'never', so a template that marks common lossless formats 'required' would forbid native streaming
     * of them for everyone the moment a transcoder is picked during install, without any further opt-in.
     */
    public function testInstallConfigTranscodeModeKeepsNativeStreamingAvailable(): void
    {
        $this->subject->install_config_transcode_mode('ffmpeg');

        self::assertSame('allowed', AmpConfig::get('transcode_flac'));
        self::assertSame('allowed', AmpConfig::get('transcode_m4a'));
        self::assertSame('allowed', AmpConfig::get('transcode_mpc'));
        self::assertSame('allowed', AmpConfig::get('transcode_wav'));

        AmpConfig::set('transcode_player_webplayer_flac', '', true);
        AmpConfig::set('encode_player_webplayer_target', '', true);

        self::assertSame(['native', 'transcode'], Stream::get_stream_types_for_type('flac', 'webplayer'));
    }

    /**
     * Invalid database settings must fail before anything is echoed, the same as an unwritable rewrite file,
     * so a download request that never actually downloads still leaves the caller able to show the error.
     */
    public function testInstallCreateConfigEchoesNothingWhenValidationFails(): void
    {
        AmpConfig::set('database_username', '', true);
        AmpConfig::set('database_password', '', true);
        AmpConfig::set('database_hostname', 'localhost', true);

        ob_start();
        $result = $this->subject->install_create_config(true);
        $output = ob_get_clean();

        self::assertFalse($result);
        self::assertSame('', $output);
    }

    /**
     * A failed validation must echo nothing, so the caller can safely fall through to the normal error page
     * instead of returning a blank response as though a file had already been streamed to the browser.
     */
    public function testInstallRewriteRulesEchoesNothingWhenValidationFails(): void
    {
        ob_start();
        $result = $this->subject->install_rewrite_rules(sys_get_temp_dir() . '/nonexistent-htaccess', '/ampache', true);
        $output = ob_get_clean();

        self::assertFalse($result);
        self::assertSame('', $output);
    }

    /**
     * A download that actually streamed the file must report success, not just "not a write failure". The
     * install page used the return value to decide whether to render itself afterwards; a false here would
     * make it treat an already-sent download the same as a validation failure that echoed nothing at all.
     */
    public function testInstallRewriteRulesReportsSuccessAfterStreamingADownload(): void
    {
        $file = $this->writeRules('RewriteRule ^image\.php$ /image.php [R=302,L]');
        copy($file, $file . '.dist');

        ob_start();
        $result = $this->subject->install_rewrite_rules($file, '/ampache', true);
        $output = ob_get_clean();

        self::assertTrue($result);
        self::assertStringContainsString('/ampache/image.php', $output);
    }

    public function testRealTargetsGainTheWebPath(): void
    {
        self::assertSame(
            'RewriteRule ^image\.php$ /ampache/image.php?action=show_user_avatar [R=302,L]',
            $this->subject->install_check_rewrite_rules(
                $this->writeRules('RewriteRule ^image\.php$ /image.php?action=show_user_avatar [R=302,L]'),
                '/ampache',
                true
            )
        );
    }

    public function testShippedRestRulesValidateAgainstTheirOwnDist(): void
    {
        $dist = __DIR__ . '/../../../public/rest/.htaccess.dist';
        $file = $this->writeRules((string) file_get_contents($dist));
        copy($dist, $file . '.dist');

        self::assertTrue($this->subject->install_check_rewrite_rules($file, ''));
    }

    /**
     * A file left over from an older release keeps the right web path on the rules it does have, so the prefix
     * test alone reports it as configured while every rule added since is missing.
     */
    public function testValidationFailsForAFileMissingRulesTheDistDeclares(): void
    {
        $file = $this->writeRules('RewriteRule ^old\.php$ /ampache/old.php [L]');
        file_put_contents(
            $file . '.dist',
            implode("\n", [
                'RewriteRule ^old\.php$ /old.php [L]',
                'RewriteRule ^new\.php$ /new.php [L]',
            ])
        );

        self::assertFalse($this->subject->install_check_rewrite_rules($file, '/ampache'));
    }

    public function testValidationFailsForAnUnprefixedTarget(): void
    {
        self::assertFalse(
            $this->subject->install_check_rewrite_rules($this->writeRules('RewriteRule ^image\.php$ /image.php [R=302,L]'), '/ampache')
        );
    }

    public function testValidationFailsWhenTheFileHasNotBeenWrittenYet(): void
    {
        self::assertFalse(
            $this->subject->install_check_rewrite_rules(sys_get_temp_dir() . '/nonexistent-htaccess', '')
        );
    }

    public function testValidationPassesForAFileCarryingEveryDistRule(): void
    {
        $file = $this->writeRules(
            implode("\n", [
                'RewriteRule ^old\.php$ /ampache/old.php [L]',
                'RewriteRule ^new\.php$ /ampache/new.php [L]',
            ])
        );
        file_put_contents(
            $file . '.dist',
            implode("\n", [
                'RewriteRule ^old\.php$ /old.php [L]',
                'RewriteRule ^new\.php$ /new.php [L]',
            ])
        );

        self::assertTrue($this->subject->install_check_rewrite_rules($file, '/ampache'));
    }

    /**
     * The same rules read in validation mode, which is what the install page reports as "configured?"
     */
    public function testValidationPassesForFlagOnlyRules(): void
    {
        self::assertTrue(
            $this->subject->install_check_rewrite_rules($this->writeRules('RewriteRule (.*) - [R=400,L]'), '/ampache')
        );
    }

    #[Override]
    protected function setUp(): void
    {
        $this->subject = new InstallationHelper(
            $this->mock(UpdaterInterface::class),
            $this->mock(UpdateInfoRepositoryInterface::class),
        );

        // AmpError::$errors is static and outlives a test, so a prior test's 'general' error (if any) makes
        // AmpError::add() append rather than set; seed the session key it would append onto either way.
        $_SESSION = ['errors' => ['general' => '']];
    }

    #[Override]
    protected function tearDown(): void
    {
        foreach ([$this->file, $this->file . '.dist'] as $path) {
            if ($this->file !== '' && file_exists($path)) {
                unlink($path);
            }
        }

        if ($this->configFile !== '' && file_exists($this->configFile)) {
            unlink($this->configFile);
        }
    }

    private function writeConfig(string $contents): string
    {
        $this->configFile = (string) tempnam(sys_get_temp_dir(), 'ampachecfg');
        file_put_contents($this->configFile, $contents);

        return $this->configFile;
    }

    private function writeRules(string $rules): string
    {
        $this->file = (string) tempnam(sys_get_temp_dir(), 'htaccess');
        file_put_contents($this->file, $rules);

        return $this->file;
    }
}