. * */ namespace Ampache\Repository; use Ampache\Module\Database\DatabaseConnectionInterface; use Ampache\Repository\Model\Mood; use PDOStatement; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use SEEC\PhpUnit\Helper\ConsecutiveParams; class MoodRepositoryTest extends TestCase { use ConsecutiveParams; private DatabaseConnectionInterface&MockObject $connection; private MoodRepository $subject; public function testAddMapReturnsTheNewMapId(): void { $this->connection->expects(static::once()) ->method('query') ->with( 'INSERT IGNORE INTO `mood_map` (`mood_id`, `user`, `object_type`, `object_id`) VALUES (?, ?, ?, ?)', [666, 0, 'song', 42] ); $this->connection->expects(static::once()) ->method('getLastInsertedId') ->willReturn(7); self::assertSame(7, $this->subject->addMap(666, 'song', 42, 0)); } public function testCollectGarbageSweepsEveryMappableTypeAndDropsTheEmptyMoods(): void { $statements = []; $this->connection->method('query') ->willReturnCallback(function (string $sql) use (&$statements): PDOStatement { $statements[] = $sql; return $this->createMock(PDOStatement::class); }); $this->subject->collectGarbage(); // a map is orphaned by whichever object it names, and a type with no sweep keeps the mood alive for ever foreach (Mood::OBJECT_TYPES as $objectType) { self::assertNotEmpty( array_filter( $statements, static fn(string $sql): bool => str_starts_with($sql, 'DELETE FROM `mood_map`') && str_contains($sql, sprintf("`mood_map`.`object_type`='%s'", $objectType)) ), sprintf('no orphaned map sweep for %s', $objectType) ); } // a mood nothing points at any more goes, whoever created it self::assertNotEmpty( array_filter($statements, static fn(string $sql): bool => str_starts_with($sql, 'DELETE FROM `mood` USING `mood`')) ); // a write for a type the enum does not hold is truncated to the error value, and no other sweep can resolve it self::assertNotEmpty( array_filter($statements, static fn(string $sql): bool => str_starts_with($sql, 'DELETE FROM `mood_map`') && str_contains($sql, "`object_type` = ''")) ); // the owner is part of `unique_mood_map`, so the duplicate sweep must not take the map a user set beside the one from the file self::assertNotEmpty( array_filter( $statements, static fn(string $sql): bool => str_starts_with($sql, 'DELETE `b` FROM `mood_map`') && str_contains($sql, '`a`.`user` <=> `b`.`user`') ) ); } public function testGetTopMoodsBulkDoesNothingForNoObjects(): void { $this->connection->expects(static::never()) ->method('query'); self::assertSame([], $this->subject->getTopMoodsBulk('song', [])); } public function testGetTopMoodsBulkKeysTheRowsByObjectAndKeepsTheEmptyOnes(): void { $result = $this->createMock(PDOStatement::class); $this->connection->expects(static::once()) ->method('query') ->with( self::callback( static fn(string $sql): bool => str_contains($sql, '`mood_map`.`object_id` AS `owner_id`') && str_contains($sql, '`mood_map`.`object_id` IN (?,?)') ), ['song', 42, 43] ) ->willReturn($result); $result->method('fetch')->willReturnOnConsecutiveCalls( ['owner_id' => '43', 'id' => '7', 'name' => 'calm', 'user' => '0', 'count' => '3'], false ); self::assertSame( [ 42 => [], 43 => [['id' => 7, 'name' => 'calm', 'user' => 0, 'count' => 3]], ], $this->subject->getTopMoodsBulk('song', [42, 43]) ); } public function testGetTopMoodsGroupsTheMapsOfOneMoodIntoASingleRow(): void { $result = $this->createMock(PDOStatement::class); $this->connection->expects(static::once()) ->method('query') ->with( self::callback( static fn(string $sql): bool => str_contains($sql, 'MAX(`mood_map`.`user`) AS `user`') && str_contains($sql, 'GROUP BY `mood`.`id`') ), ['song', 42] ) ->willReturn($result); $result->method('fetch')->willReturn(false); self::assertSame([], $this->subject->getTopMoods('song', 42, 0)); } public function testMigrateMapsKeepsWhatTheTargetAlreadyHad(): void { // a user who already set this mood on both sides keeps it instead of the destination row being silently dropped $this->connection->expects(static::exactly(2)) ->method('query') ->with( ...self::withConsecutive( [ 'INSERT IGNORE INTO `mood_map` (`mood_id`, `object_id`, `object_type`, `user`) SELECT `old_map`.`mood_id`, ?, `old_map`.`object_type`, `old_map`.`user` FROM `mood_map` AS `old_map` WHERE `old_map`.`object_type` = ? AND `old_map`.`object_id` = ?', [42, 'song', 666], ], [ 'DELETE FROM `mood_map` WHERE `object_type` = ? AND `object_id` = ?', ['song', 666], ], ) ); $this->subject->migrateMaps('song', 666, 42); } protected function setUp(): void { $this->connection = $this->createMock(DatabaseConnectionInterface::class); $this->subject = new MoodRepository($this->connection); } }