tests/Repository/CatalogRepositoryTest.php
Rama develop · réplica de lectura del repositorio original.
<?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\Config\ConfigContainerInterface;
use Ampache\Module\Catalog\CatalogTypeEnum;
use Ampache\Module\Database\DatabaseConnectionInterface;
use Ampache\Module\Database\Exception\InsertIdInvalidException;
use Ampache\Module\Database\Exception\QueryFailedException;
use Ampache\Repository\Model\CatalogFieldEnum;
use PDOStatement;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ValueError;
class CatalogRepositoryTest extends TestCase
{
private ConfigContainerInterface&MockObject $configContainer;
private DatabaseConnectionInterface&MockObject $connection;
private CatalogRepository $subject;
public function testActionLockNameStaysWithinTheLimitMysqlEnforces(): void
{
$lockName = null;
$this->connection->expects(static::once())
->method('fetchOne')
->willReturnCallback(function (string $sql, array $params) use (&$lockName): string {
$lockName = $params[0];
return '1';
});
$this->subject->tryAcquireActionLock(str_repeat('a', 200));
self::assertLessThanOrEqual(64, strlen((string) $lockName));
}
public function testCreateSubTypeTableRefusesAColumnNoBackendDeclares(): void
{
$this->configContainer->method('get')->willReturn('utf8mb4');
$this->connection->expects(static::never())
->method('query');
static::expectException(ValueError::class);
$this->subject->createSubTypeTable(CatalogTypeEnum::LOCAL, ['DROP TABLE `song`' => 'INT']);
}
public function testCreateSubTypeTableWrapsTheBackendsColumnsAndCollatesTheStrings(): void
{
$this->configContainer->method('get')->willReturnCallback(static fn(string $key): string => match ($key) {
'database_collation' => 'utf8mb4_unicode_ci',
'database_charset' => 'utf8mb4',
'database_engine' => 'InnoDB',
default => '',
});
$this->connection->expects(static::once())
->method('query')
->with('CREATE TABLE `catalog_dropbox` (`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `apikey` VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL, `getchunk` TINYINT(1) NOT NULL, `catalog_id` INT(11) NOT NULL) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci');
$this->subject->createSubTypeTable(CatalogTypeEnum::DROPBOX, ['apikey' => 'VARCHAR(255)', 'getchunk' => 'TINYINT(1)']);
}
public function testDeleteSubTypeRowNamesTheBackendsOwnTable(): void
{
$this->connection->expects(static::once())
->method('query')
->with('DELETE FROM `catalog_seafile` WHERE `catalog_id` = ?', [7]);
self::assertTrue($this->subject->deleteSubTypeRow(CatalogTypeEnum::SEAFILE, 7));
}
public function testDeleteSubTypeRowReportsAFailure(): void
{
$this->connection->expects(static::once())
->method('query')
->willThrowException(new QueryFailedException('nope'));
self::assertFalse($this->subject->deleteSubTypeRow(CatalogTypeEnum::LOCAL, 7));
}
public function testDropSubTypeTableToleratesATableThatIsAlreadyGone(): void
{
$this->connection->expects(static::once())
->method('query')
->with('DROP TABLE `catalog_subsonic`')
->willThrowException(new QueryFailedException('no such table'));
$this->subject->dropSubTypeTable(CatalogTypeEnum::SUBSONIC);
}
public function testFindEnabledSeparatesADisabledCatalogFromAMissingOne(): void
{
$this->connection->expects(static::exactly(2))
->method('fetchOne')
->with('SELECT `enabled` FROM `catalog` WHERE `id` = ?', [7])
->willReturn('0', false);
self::assertFalse($this->subject->findEnabled(7));
self::assertNull($this->subject->findEnabled(7));
}
public function testFindNameFallsBackToAnEmptyString(): void
{
$this->connection->expects(static::once())
->method('fetchOne')
->with('SELECT `name` FROM `catalog` WHERE `id` = ?', [666])
->willReturn(false);
self::assertSame('', $this->subject->findName(666));
}
public function testFindSubTypeIdReturnsNullWhenTheBackendsTableIsGone(): void
{
// uninstalling a backend drops its settings table but can leave catalog rows pointing at it
$this->connection->expects(static::once())
->method('fetchOne')
->willThrowException(new QueryFailedException('no such table'));
self::assertNull($this->subject->findSubTypeId(CatalogTypeEnum::SUBSONIC, 7));
}
public function testFindTypeReturnsNullForAMissingCatalog(): void
{
$this->connection->expects(static::once())
->method('fetchOne')
->with('SELECT `catalog_type` FROM `catalog` WHERE `id` = ?', [666])
->willReturn(false);
self::assertNull($this->subject->findType(666));
}
public function testGetIdsAppliesNoWhereClauseWithoutNarrowing(): void
{
$result = $this->createMock(PDOStatement::class);
$this->connection->expects(static::once())
->method('query')
->with('SELECT `id` FROM `catalog` ORDER BY `name`;', [])
->willReturn($result);
$result->expects(static::exactly(2))
->method('fetchColumn')
->willReturn('3', false);
self::assertSame([3], $this->subject->getIds());
}
public function testGetIdsHoldsTheSystemUserToTheDefaultFilterGroup(): void
{
$result = $this->createMock(PDOStatement::class);
$this->connection->expects(static::once())
->method('query')
->with(
self::stringContains('`catalog_filter_group_map`.`group_id` = 0'),
[]
)
->willReturn($result);
$result->expects(static::once())
->method('fetchColumn')
->willReturn(false);
self::assertSame([], $this->subject->getIds(null, false, -1));
}
public function testGetIdsIgnoresTheFilterForUserZero(): void
{
$result = $this->createMock(PDOStatement::class);
// a user id of 0 means "no user", and never filtered in the version this replaced
$this->connection->expects(static::once())
->method('query')
->with('SELECT `id` FROM `catalog` ORDER BY `name`;', [])
->willReturn($result);
$result->expects(static::once())
->method('fetchColumn')
->willReturn(false);
self::assertSame([], $this->subject->getIds(null, false, 0));
}
public function testGetIdsJoinsTheUsersOwnFilterGroup(): void
{
$result = $this->createMock(PDOStatement::class);
$this->connection->expects(static::once())
->method('query')
->with(
self::stringContains('INNER JOIN `user` ON `user`.`catalog_filter_group` = `catalog_filter_group_map`.`group_id`'),
[42]
)
->willReturn($result);
$result->expects(static::once())
->method('fetchColumn')
->willReturn(false);
self::assertSame([], $this->subject->getIds(null, false, 42));
}
public function testGetIdsStacksTheGatherTypeAndEnabledNarrowing(): void
{
$result = $this->createMock(PDOStatement::class);
$this->connection->expects(static::once())
->method('query')
->with(
'SELECT `id` FROM `catalog` WHERE `gather_types` = ? AND `enabled` = 1 ORDER BY `name`;',
['music']
)
->willReturn($result);
$result->expects(static::once())
->method('fetchColumn')
->willReturn(false);
self::assertSame([], $this->subject->getIds('music', true));
}
public function testInsertReturnsZeroWhenTheRowProducedNoId(): void
{
$this->connection->expects(static::once())
->method('query');
$this->connection->expects(static::once())
->method('getLastInsertedId')
->willThrowException(new InsertIdInvalidException());
self::assertSame(0, $this->subject->insert('some-catalog', 'local', '', '', 'music'));
}
public function testInsertSubTypeAppendsTheCatalogIdAndBindsEveryValue(): void
{
$this->connection->expects(static::once())
->method('query')
->with(
'INSERT INTO `catalog_remote` (`uri`, `username`, `password`, `catalog_id`) VALUES (?, ?, ?, ?)',
['http://host', 'user', 'pass', 7]
);
self::assertTrue($this->subject->insertSubType(
CatalogTypeEnum::REMOTE,
['uri' => 'http://host', 'username' => 'user', 'password' => 'pass'],
7
));
}
public function testInsertWritesTheRowAndReturnsItsId(): void
{
$this->connection->expects(static::once())
->method('query')
->with(
'INSERT INTO `catalog` (`name`, `catalog_type`, `rename_pattern`, `sort_pattern`, `gather_types`) VALUES (?, ?, ?, ?, ?)',
['some-catalog', 'local', '%T', '%a', 'music']
);
$this->connection->expects(static::once())
->method('getLastInsertedId')
->willReturn(4);
self::assertSame(4, $this->subject->insert('some-catalog', 'local', '%T', '%a', 'music'));
}
public function testIsActionProcessingReflectsWhetherAnyConnectionHoldsTheLock(): void
{
$this->connection->expects(static::exactly(2))
->method('fetchOne')
->with('SELECT IS_USED_LOCK(?)', ['ampache_sse_action_' . md5('some-key')])
->willReturn('42', null);
self::assertTrue($this->subject->isActionProcessing('some-key'));
self::assertFalse($this->subject->isActionProcessing('some-key'));
}
public function testReleaseActionLockCallsReleaseLockWithTheNamespacedName(): void
{
$this->connection->expects(static::once())
->method('query')
->with('SELECT RELEASE_LOCK(?)', ['ampache_sse_action_' . md5('some-key')]);
$this->subject->releaseActionLock('some-key');
}
public function testReleaseProcessingLockCallsReleaseLockWithTheNamespacedName(): void
{
$this->connection->expects(static::once())
->method('query')
->with('SELECT RELEASE_LOCK(?)', ['ampache_catalog_7']);
$this->subject->releaseProcessingLock(7);
}
public function testSetFieldNamesOnlyABoundedColumn(): void
{
$this->connection->expects(static::once())
->method('query')
->with('UPDATE `catalog` SET `last_update` = ? WHERE `id` = ?', [123456, 7]);
self::assertTrue($this->subject->setField(7, CatalogFieldEnum::LAST_UPDATE, 123456));
}
public function testSubTypeTableExistsAsksForTheBackendsOwnTable(): void
{
$this->connection->expects(static::once())
->method('fetchOne')
->with("SHOW TABLES LIKE 'catalog_beetsremote'")
->willReturn(false);
self::assertFalse($this->subject->subTypeTableExists(CatalogTypeEnum::BEETSREMOTE));
}
public function testSubTypeValueExistsChecksTheBackendsOwnTable(): void
{
// the beetsremote check used to name `catalog_beets` and a column it has no such column
$this->connection->expects(static::once())
->method('fetchOne')
->with('SELECT `id` FROM `catalog_beetsremote` WHERE `uri` = ?', ['http://host'])
->willReturn('1');
self::assertTrue($this->subject->subTypeValueExists(CatalogTypeEnum::BEETSREMOTE, 'uri', 'http://host'));
}
public function testTryAcquireActionLockReflectsWhoWonTheLock(): void
{
$this->connection->expects(static::exactly(2))
->method('fetchOne')
->with('SELECT GET_LOCK(?, 0)', ['ampache_sse_action_' . md5('some-key')])
->willReturn('1', '0');
self::assertTrue($this->subject->tryAcquireActionLock('some-key'));
self::assertFalse($this->subject->tryAcquireActionLock('some-key'));
}
public function testTryAcquireProcessingLockReflectsWhoWonTheLock(): void
{
$this->connection->expects(static::exactly(2))
->method('fetchOne')
->with('SELECT GET_LOCK(?, 0)', ['ampache_catalog_7'])
->willReturn('1', '0');
self::assertTrue($this->subject->tryAcquireProcessingLock(7));
self::assertFalse($this->subject->tryAcquireProcessingLock(7));
}
public function testUpdateSettingsWritesTheThreeFormFields(): void
{
$this->connection->expects(static::once())
->method('query')
->with(
'UPDATE `catalog` SET `name` = ?, `rename_pattern` = ?, `sort_pattern` = ? WHERE `id` = ?',
['renamed', '%T', '%a', 7]
);
$this->subject->updateSettings(7, 'renamed', '%T', '%a');
}
protected function setUp(): void
{
$this->connection = $this->createMock(DatabaseConnectionInterface::class);
$this->configContainer = $this->createMock(ConfigContainerInterface::class);
$this->subject = new CatalogRepository($this->connection, $this->configContainer);
}
}