Ctrl K
ComunidadGuías…

tests/Repository/CatalogMapRepositoryTest.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\Repository;

use Ampache\Module\Catalog\CatalogMapTableEnum;
use Ampache\Module\Database\DatabaseConnectionInterface;
use Ampache\Module\Database\Exception\QueryFailedException;
use PDOStatement;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use SEEC\PhpUnit\Helper\ConsecutiveParams;

class CatalogMapRepositoryTest extends TestCase
{
    use ConsecutiveParams;

    private DatabaseConnectionInterface&MockObject $connection;
    private LoggerInterface&MockObject $logger;
    private CatalogMapRepository $subject;

    public function testAddBindsEveryValue(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with(
                'INSERT IGNORE INTO `catalog_map` (`catalog_id`, `object_type`, `object_id`) VALUES (?, ?, ?);',
                [7, 'podcast', 666]
            );

        $this->subject->add(7, 'podcast', 666);
    }

    public function testAddForArtistBindsTheArtistOncePerUnionBranch(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with(
                self::stringContains('INSERT IGNORE INTO `catalog_map`'),
                [42, 42, 42, 42]
            );

        $this->subject->addForArtist(42);
    }

    public function testCollectGarbageAlwaysSweepsCatalogZeroEvenWithNoTables(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with('DELETE FROM `catalog_map` WHERE `catalog_id` = 0');

        $this->subject->collectGarbage([]);
    }

    public function testCollectGarbageCarriesOnAfterAFailedStatement(): void
    {
        $calls = [];

        $this->connection->expects(static::exactly(2))
            ->method('query')
            ->willReturnCallback(function (string $sql) use (&$calls): PDOStatement {
                $calls[] = $sql;
                if (count($calls) === 1) {
                    throw new QueryFailedException('nope');
                }

                return $this->createMock(PDOStatement::class);
            });

        $this->logger->expects(static::once())
            ->method('debug');

        $this->subject->collectGarbage([CatalogMapTableEnum::VIDEO]);

        self::assertCount(2, $calls);
        self::assertSame('DELETE FROM `catalog_map` WHERE `catalog_id` = 0', $calls[1]);
    }

    public function testCollectGarbageDerivesThePlaylistCatalogFromItsSongs(): void
    {
        $calls = [];

        $this->connection->expects(static::exactly(2))
            ->method('query')
            ->willReturnCallback(function (string $sql) use (&$calls): PDOStatement {
                $calls[] = $sql;

                return $this->createMock(PDOStatement::class);
            });

        $this->subject->collectGarbage([CatalogMapTableEnum::PLAYLIST]);

        self::assertSame(
            "DELETE FROM `catalog_map` USING `catalog_map` LEFT JOIN (SELECT DISTINCT `song`.`catalog` AS `catalog_id`, `playlist`.`id` AS `object_id` FROM `playlist` LEFT JOIN `playlist_data` ON `playlist`.`id` = `playlist_data`.`playlist` LEFT JOIN `song` ON `song`.`id` = `playlist_data`.`object_id` AND `playlist_data`.`object_type` = 'song') AS `valid_maps` ON `valid_maps`.`catalog_id` = `catalog_map`.`catalog_id` AND `valid_maps`.`object_id` = `catalog_map`.`object_id` WHERE `catalog_map`.`object_type` = 'playlist' AND `valid_maps`.`object_id` IS NULL;",
            $calls[0]
        );
    }

    public function testCollectGarbageForObjectChecksOnlyTheGivenIdForANonArtistType(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with(
                'DELETE FROM `catalog_map` WHERE `object_type` = ? AND `object_id` = ? AND NOT EXISTS (SELECT 1 FROM `podcast_episode` WHERE `id` = ?);',
                ['podcast_episode', 666, 666]
            );

        $this->subject->collectGarbageForObject(CatalogMapTableEnum::PODCAST_EPISODE, 666);
    }

    public function testCollectGarbageForObjectSweepsTheThreeArtistRolesScopedToOneId(): void
    {
        $calls = [];

        $this->connection->expects(static::exactly(3))
            ->method('query')
            ->willReturnCallback(function (string $sql, array $params) use (&$calls): PDOStatement {
                $calls[] = [$sql, $params];

                return $this->createMock(PDOStatement::class);
            });

        $this->subject->collectGarbageForObject(CatalogMapTableEnum::ARTIST, 42);

        self::assertStringContainsString("`object_type` = 'album_artist'", $calls[0][0]);
        self::assertSame([42, 42], $calls[0][1]);
        self::assertStringContainsString("`object_type` = 'song_artist'", $calls[1][0]);
        self::assertSame([42, 42], $calls[1][1]);
        self::assertStringContainsString("`object_type` = 'artist'", $calls[2][0]);
        self::assertSame([42, 42], $calls[2][1]);
    }

    public function testCollectGarbageSweepsTheThreeArtistRolesBeforeCatalogZero(): void
    {
        $calls = [];

        $this->connection->expects(static::exactly(4))
            ->method('query')
            ->willReturnCallback(function (string $sql) use (&$calls): PDOStatement {
                $calls[] = $sql;

                return $this->createMock(PDOStatement::class);
            });

        $this->subject->collectGarbage([CatalogMapTableEnum::ARTIST]);

        self::assertStringContainsString("`object_type` = 'album_artist'", $calls[0]);
        self::assertStringContainsString("`object_type` = 'song_artist'", $calls[1]);
        self::assertStringContainsString("`object_type` = 'artist'", $calls[2]);
        self::assertSame('DELETE FROM `catalog_map` WHERE `catalog_id` = 0', $calls[3]);
    }

    public function testCollectGarbageUsesTheTableNameOfANonArtistType(): void
    {
        $calls = [];

        $this->connection->expects(static::exactly(2))
            ->method('query')
            ->willReturnCallback(function (string $sql) use (&$calls): PDOStatement {
                $calls[] = $sql;

                return $this->createMock(PDOStatement::class);
            });

        $this->subject->collectGarbage([CatalogMapTableEnum::PODCAST_EPISODE]);

        self::assertSame(
            "DELETE FROM `catalog_map` USING `catalog_map` LEFT JOIN (SELECT DISTINCT `podcast_episode`.`catalog` AS `catalog_id`, `podcast_episode`.`id` AS `object_id` FROM `podcast_episode`) AS `valid_maps` ON `valid_maps`.`catalog_id` = `catalog_map`.`catalog_id` AND `valid_maps`.`object_id` = `catalog_map`.`object_id` WHERE `catalog_map`.`object_type` = 'podcast_episode' AND `valid_maps`.`object_id` IS NULL;",
            $calls[0]
        );
    }

    public function testDeleteForObjectRemovesEveryMappingOfIt(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with(
                'DELETE FROM `catalog_map` WHERE `object_id` = ? AND `object_type` = ?',
                [666, 'album']
            );

        $this->subject->deleteForObject('album', 666);
    }

    public function testMigrateKeepsWhatTheTargetAlreadyHad(): void
    {
        $this->connection->expects(static::exactly(2))
            ->method('query')
            ->with(
                ...self::withConsecutive(
                    [
                        'INSERT IGNORE INTO `catalog_map` (`object_id`, `object_type`, `catalog_id`) SELECT ?, `old_map`.`object_type`, `old_map`.`catalog_id` FROM `catalog_map` AS `old_map` WHERE `old_map`.`object_type` = ? AND `old_map`.`object_id` = ?',
                        [42, 'artist', 666],
                    ],
                    [
                        'DELETE FROM `catalog_map` WHERE `object_type` = ? AND `object_id` = ?',
                        ['artist', 666],
                    ],
                )
            );

        self::assertTrue($this->subject->migrate('artist', 666, 42));
    }

    public function testMigrateReportsAFailedUpdate(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->willThrowException(new QueryFailedException('nope'));

        self::assertFalse($this->subject->migrate('artist', 666, 42));
    }

    public function testRebuildDerivesThePlaylistCatalogFromItsSongs(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with(self::stringContains("SELECT `song`.`catalog`, 'playlist', `playlist`.`id` FROM `playlist`"));

        $this->subject->rebuild(CatalogMapTableEnum::PLAYLIST);
    }

    public function testRebuildTakesTheCatalogOffTheTableItself(): void
    {
        $this->connection->expects(static::once())
            ->method('query')
            ->with("INSERT IGNORE INTO `catalog_map` (`catalog_id`, `object_type`, `object_id`) SELECT `album_disk`.`catalog`, 'album_disk', `album_disk`.`id` FROM `album_disk` GROUP BY `album_disk`.`catalog`, 'album_disk', `album_disk`.`id`;");

        $this->subject->rebuild(CatalogMapTableEnum::ALBUM_DISK);
    }

    protected function setUp(): void
    {
        $this->connection = $this->createMock(DatabaseConnectionInterface::class);
        $this->logger     = $this->createMock(LoggerInterface::class);

        $this->subject = new CatalogMapRepository(
            $this->connection,
            $this->logger
        );
    }
}