tests/Repository/RatingRepositoryTest.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\Module\Database\DatabaseConnectionInterface;
use PDO;
use PDOStatement;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use SEEC\PhpUnit\Helper\ConsecutiveParams;
class RatingRepositoryTest extends TestCase
{
use ConsecutiveParams;
private DatabaseConnectionInterface&MockObject $connection;
private LoggerInterface&MockObject $logger;
private RatingRepository $subject;
public function testAdjustWeightFloorsTheDecrementAtZero(): void
{
// an unbalanced sequence of removals must never push the weight below zero
$this->connection->expects(static::once())
->method('query')
->with(
'UPDATE `song` SET `weight` = GREATEST(`weight` - 1, 0) WHERE `id` = ?;',
[666]
);
$this->subject->adjustWeight('song', 666, -1);
}
public function testAdjustWeightIgnoresATypeWithoutTheColumn(): void
{
$this->connection->expects(static::never())
->method('query');
$this->subject->adjustWeight('playlist', 666, 1);
}
public function testAdjustWeightRaisesTheCounterOnTheRatedTable(): void
{
$this->connection->expects(static::once())
->method('query')
->with(
'UPDATE `song` SET `weight` = `weight` + 1 WHERE `id` = ?;',
[666]
);
$this->subject->adjustWeight('song', 666, 1);
}
public function testCollectGarbageRefusesAnUnsupportedType(): void
{
$this->connection->expects(static::never())
->method('query');
$this->logger->expects(static::once())
->method('critical');
$this->subject->collectGarbage('some-type', 666);
}
public function testCollectGarbageSweepsOneObjectAndTheEmptyRatings(): 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('song', 666);
self::assertSame(
[
'DELETE FROM `rating` WHERE `object_type` = ? AND `object_id` = ?',
'DELETE FROM `rating` WHERE `rating`.`rating` = 0;',
],
$calls
);
}
public function testGetAverageRatingCountsItsVotersFromTheSameQuery(): void
{
// the HAVING already counts them, so the number of voters costs nothing to read back
$this->connection->expects(static::once())
->method('fetchRow')
->willReturn(['rating' => '4.14', 'voters' => '7']);
self::assertSame([4.14, 7], $this->subject->getAverageRating(666, 'song'));
}
public function testGetAverageRatingReturnsNullWhileOnlyOneUserHasRated(): void
{
$this->connection->expects(static::once())
->method('fetchRow')
->with(
'SELECT ROUND(AVG(`rating`), 2) AS `rating`, COUNT(`object_id`) AS `voters` FROM `rating` WHERE `object_id` = ? AND `object_type` = ? HAVING COUNT(`object_id`) > 1',
[666, 'song']
)
->willReturn(false);
self::assertNull($this->subject->getAverageRating(666, 'song'));
}
public function testGetAverageRatingsNarrowsTheSameWayTheSingleObjectQueryDoes(): void
{
$result = $this->createMock(PDOStatement::class);
// without the HAVING clause the batch that warms the cache disagrees with getAverageRating(),
// so a browse showed a rounded average where the object's own page showed none at all
$this->connection->expects(static::once())
->method('query')
->with(
'SELECT ROUND(AVG(`rating`), 2) AS `rating`, COUNT(`object_id`) AS `voters`, `object_id` FROM `rating` WHERE `object_id` IN (1,2) AND `object_type` = ? GROUP BY `object_id` HAVING COUNT(`object_id`) > 1',
['song']
)
->willReturn($result);
$result->expects(static::exactly(2))
->method('fetch')
->willReturn(['object_id' => '1', 'rating' => '3.67', 'voters' => '3'], false);
self::assertSame([1 => [3.67, 3]], $this->subject->getAverageRatings('song', [1, 2]));
}
public function testGetUserRatingReturnsTheStoredValue(): void
{
$this->connection->expects(static::once())
->method('fetchOne')
->with(
'SELECT `rating` FROM `rating` WHERE `user` = ? AND `object_id` = ? AND `object_type` = ? AND `rating` > 0;',
[42, 666, 'song']
)
->willReturn('4');
self::assertSame(4, $this->subject->getUserRating(666, 'song', 42));
}
public function testGetUserRatingsKeysTheRowsByObjectAndCastsTheIdList(): void
{
$result = $this->createMock(PDOStatement::class);
$this->connection->expects(static::once())
->method('query')
->with(
'SELECT `rating`, `object_id` FROM `rating` WHERE `user` = ? AND `object_id` IN (1,2) AND `object_type` = ?',
[42, 'song']
)
->willReturn($result);
$result->expects(static::exactly(2))
->method('fetch')
->with(PDO::FETCH_ASSOC)
->willReturn(['object_id' => '2', 'rating' => '5'], false);
self::assertSame(
[2 => 5],
$this->subject->getUserRatings('song', [1, '2'], 42)
);
}
public function testGetUserRatingsSkipsTheQueryForAnEmptyList(): void
{
$this->connection->expects(static::never())
->method('query');
self::assertSame([], $this->subject->getUserRatings('song', [], 42));
}
public function testMigrateMovesTheRatingsKeepingWhatTheTargetHad(): void
{
// a user who rated both sides keeps the higher rating instead of the destination row being silently dropped
$this->connection->expects(static::exactly(2))
->method('query')
->with(
...self::withConsecutive(
[
'INSERT INTO `rating` (`object_id`, `object_type`, `rating`, `user`, `date`) SELECT ?, `old_rating`.`object_type`, `old_rating`.`rating`, `old_rating`.`user`, `old_rating`.`date` FROM `rating` AS `old_rating` WHERE `old_rating`.`object_type` = ? AND `old_rating`.`object_id` = ? ON DUPLICATE KEY UPDATE `rating` = GREATEST(`rating`.`rating`, VALUES(`rating`)), `date` = GREATEST(`rating`.`date`, VALUES(`date`))',
[42, 'song', 666],
],
[
'DELETE FROM `rating` WHERE `object_type` = ? AND `object_id` = ?',
['song', 666],
],
)
);
$this->subject->migrate('song', 666, 42);
}
public function testSetRatingReplacesThePreviousOne(): void
{
$this->connection->expects(static::once())
->method('query')
->with(
'REPLACE INTO `rating` (`object_id`, `object_type`, `rating`, `user`, `date`) VALUES (?, ?, ?, ?, ?)',
[666, 'song', 4, 42, 123456]
);
$this->subject->setRating(666, 'song', 4, 42, 123456);
}
protected function setUp(): void
{
$this->connection = $this->createMock(DatabaseConnectionInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->subject = new RatingRepository(
$this->connection,
$this->logger
);
}
}