what the fuck
This commit is contained in:
1 parent
c1b1c2f372
commit
72a99a5bcd
69 files changed
+7519
-558
No files matched your search
@@ -0,0 +1,30 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ1DfAnq5rpIGEmuuV
|
||||
TxzKdgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEGq9s3Pc5KgVqWPc
|
||||
DWBTt+kEggTQKShV0ASL1ZVSQa+fpUE8Alq65oJsTccgZu2MXFx0DaomeB5DSHQX
|
||||
MxrwmrXz+f9/17AzPD7dBw+2YP1UIXkiP6B89RvCc2g6G1yHL52ziEgtXPrYRrcs
|
||||
kwUHPbcd6X5XATJRSxmu8VOBimxmCNgAtuhPl6wb7a+YZYLv7EVYGaqAlPiySOKP
|
||||
Nnuv2ENAz958FNX2O7VyI4N+8W+DEtVo2CCo8luxb8SrxmtO7yxEMC9Fs2wZitgs
|
||||
PgCWIYy+laB0KBP0qIi0kOkuySDSQgC8psz0b2lKVS/k2AP3SkoyIXfjCSIgZYBI
|
||||
cFLSWlbbau/h+0AAuMmcWTheVvhSowFRVMJ5KyGK0Mw0+cADs4sd28OVtC8px+jS
|
||||
4Qr1D8jDpiEuSJhdJ1E9vRC6SYq03N2XjLWl/r18aydBtsdbMGGCi2aQ4KAAprNX
|
||||
siDGR5gKHYkY0Jef7K2VGe8ojcsjlXJfRR9B10A1+uvbMUIVcz3qk436jlNyBIsG
|
||||
Doqbp/Vortfvj97rPgY4VROYNbDaVa0I5etScRVuY95zurxbhHbT4sUny73+jDbN
|
||||
IxXslYKKqJ+d9tWVM9CCIkBZj075HgEnss8Yl4/59r2FjMcruNwxV9Ife019yOdO
|
||||
YSG/HEYveYOeM966avUhJw3AxpCSoeGRyKCFGcbj6Yu0T/MR69DA2vpLKrI29I5C
|
||||
w1Hig2PpWktduTCA+XlYAnl5wvJ60Z2BZg78RRZ/B7iEKmc5uEIAgFtjVNZ2QKtx
|
||||
xyrT/5b+e90ga1elPrNzzvdbPM3PPBuRGoLJjgv3wnDHDtqVvIEyU48RhD+EHG/1
|
||||
d8AsBgBIz12s/gbKuZbrINfIvMhIhNM5VZjfstid4hk58gycKfNV4rZgptpoXUQ9
|
||||
w4tmfDgmMuDIKrQYw+LuDB3kbY75RasmaQxNOLs4/40G6SBQ6stPyWjzMCUvWZk8
|
||||
9HjxbP3MvryoCb4YCMW8MwzJCiAtetB9on0d+KAh5t2fyCVXkN1fqyAW55uQVvuf
|
||||
vDRShBfX19dGiy5cFW6f9/aXpYWmtktLT+5Kreh1lGlb+2hX7qvom/ND1MFMqwXz
|
||||
nNs4P2kvOHx9xnZMxcHCJZhyvIOn8Wwv4FpX1t/Hw0YBW1LE7wUnsoL0rI7p2CCY
|
||||
1EJwQ61F4K02b12xTcbGM8LfxrsZ959eCe7XxmNUWWPQsqR1gLJxF92vTZdpO3VZ
|
||||
bEZKDQZmnAvT3BbgelYABlCl9QqTs9/9wk0IEU59D/fWsLKx9/Q5b1C5czMXyzmd
|
||||
OjGA3G4ADT2IE2GeGrL3W45wldcgTL5mxv6kCAnFsKJy2LmyOPYgFunixz8gTkOa
|
||||
G0FwAP3q71D8JPQIuYZ8k9RPiKthSxfNHaDytLzjmJwzPpikP4oo1LCuDOYlst86
|
||||
ZSTIcEk2w19J66sSOhXFs5ByHhpna1/o12cHBE+p/Xv7CM6rQq54ASk8Ss+lWCuf
|
||||
JuHck+67YOVesC4moelTlOa0yK2mll24rqSVo4D/uXkEz7VxSY+cdKFUy9KRboxl
|
||||
BU2u8npv3n6s0A0R8bOWFo3/3A64NEJ+zn+gXUGsHLD9QVTrQfUwNxqhlvVi+SOQ
|
||||
TYQKbGX5aGNLDuieqY9LAkSYTDMZ8HpJyMZXXPcjk8f6+U91shJrKpw=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqcRbql9j/cn0Wb3dOb9R
|
||||
jpgNkp1+g3ZBridR3hxCAUMB1jPrZgwL4vt4/Tr8lNrstDxMSk0KRodtD2SnergH
|
||||
MA1eDJBGhKJhIyxATrj3KhtpCw8SO3otq4JlT3gJWUTpenfEnQ9sCuJiK3muRLy5
|
||||
EDTidwPG6RHca4t4hng62P8mjE3wZUMbFJ8Nf73y4kKi5BLmpk3VZ3/FZGb9o+6C
|
||||
COXjRdeV1Ubhyd+Kxz3LQFcgVSzlqry3K8EaG0b82jyG6r9wJ/RGJJbJ67+q4jdD
|
||||
+TFBcH0FaoO8KPM+CrPpJYvYJtYTz6HoCFbDHdGMOZnwKeF9oeIoW8sz+a6Pg0k5
|
||||
hQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Api;
|
||||
|
||||
use App\Entity\Album;
|
||||
use App\Entity\AlbumTrack;
|
||||
use App\Entity\Review;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
final class ApiHardeningFunctionalTest extends WebTestCase
|
||||
{
|
||||
private KernelBrowser $client;
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::ensureKernelShutdown();
|
||||
$this->client = static::createClient();
|
||||
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$this->resetDatabase();
|
||||
}
|
||||
|
||||
public function testReviewCreateWithoutJwtReturns401Envelope(): void
|
||||
{
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/v1/reviews',
|
||||
server: ['CONTENT_TYPE' => 'application/json'],
|
||||
content: json_encode([
|
||||
'album_id' => 'missing-album',
|
||||
'title' => 'Sample',
|
||||
'content' => str_repeat('A', 30),
|
||||
'rating' => 8,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->assertErrorEnvelope(Response::HTTP_UNAUTHORIZED, 'authentication_required');
|
||||
}
|
||||
|
||||
public function testReviewCreateWithInvalidJwtReturns401Envelope(): void
|
||||
{
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/v1/reviews',
|
||||
server: [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Bearer definitely-not-a-valid-jwt',
|
||||
],
|
||||
content: json_encode([
|
||||
'album_id' => 'missing-album',
|
||||
'title' => 'Sample',
|
||||
'content' => str_repeat('A', 30),
|
||||
'rating' => 8,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->assertErrorEnvelope(Response::HTTP_UNAUTHORIZED, 'invalid_token');
|
||||
}
|
||||
|
||||
public function testReviewCreateWithInvalidJsonReturns400Envelope(): void
|
||||
{
|
||||
$user = $this->createUser('api-invalid-json@example.test');
|
||||
$album = $this->createAlbum('album_invalid_json');
|
||||
$token = $this->issueToken($user);
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/v1/reviews',
|
||||
server: [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Bearer ' . $token,
|
||||
],
|
||||
content: sprintf('{"album_id":"%s","title":"oops"', $album->getExternalId())
|
||||
);
|
||||
|
||||
$this->assertErrorEnvelope(Response::HTTP_BAD_REQUEST, ['invalid_json', 'invalid_request']);
|
||||
}
|
||||
|
||||
public function testReviewCreateValidationReturns422EnvelopeWithFieldDetails(): void
|
||||
{
|
||||
$user = $this->createUser('api-validation@example.test');
|
||||
$album = $this->createAlbum('album_validation');
|
||||
$token = $this->issueToken($user);
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/v1/reviews',
|
||||
server: [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Bearer ' . $token,
|
||||
],
|
||||
content: json_encode([
|
||||
'album_id' => $album->getExternalId(),
|
||||
'title' => '',
|
||||
'content' => 'too short',
|
||||
'rating' => 42,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$payload = $this->assertErrorEnvelope(Response::HTTP_UNPROCESSABLE_ENTITY, 'validation_error');
|
||||
self::assertIsArray($payload['error']['details'] ?? null);
|
||||
self::assertIsArray($payload['error']['details']['fields'] ?? null);
|
||||
self::assertArrayHasKey('title', $payload['error']['details']['fields']);
|
||||
self::assertArrayHasKey('content', $payload['error']['details']['fields']);
|
||||
self::assertArrayHasKey('rating', $payload['error']['details']['fields']);
|
||||
}
|
||||
|
||||
public function testReviewUpdateByDifferentUserReturns403Envelope(): void
|
||||
{
|
||||
$owner = $this->createUser('review-owner@example.test');
|
||||
$intruder = $this->createUser('review-intruder@example.test');
|
||||
$album = $this->createAlbum('album_forbidden');
|
||||
$review = $this->createReview($album, $owner);
|
||||
$token = $this->issueToken($intruder);
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
'/api/v1/reviews/' . (string) $review->getId(),
|
||||
server: [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_AUTHORIZATION' => 'Bearer ' . $token,
|
||||
],
|
||||
content: json_encode([
|
||||
'title' => 'Unauthorized update attempt',
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->assertErrorEnvelope(Response::HTTP_FORBIDDEN, 'forbidden');
|
||||
}
|
||||
|
||||
public function testAlbumReviewsUnknownAlbumReturns404Envelope(): void
|
||||
{
|
||||
$this->client->request('GET', '/api/v1/albums/not-a-real-album-id/reviews');
|
||||
|
||||
$this->assertErrorEnvelope(Response::HTTP_NOT_FOUND, 'album_not_found');
|
||||
}
|
||||
|
||||
public function testUnsupportedMethodOnApiIndexReturns405Envelope(): void
|
||||
{
|
||||
$this->client->request('POST', '/api/v1');
|
||||
|
||||
$this->assertErrorEnvelope(Response::HTTP_METHOD_NOT_ALLOWED, 'method_not_allowed');
|
||||
}
|
||||
|
||||
public function testReviewIndexDoesNotExposeAuthorEmail(): void
|
||||
{
|
||||
$author = $this->createUser('pii-check@example.test');
|
||||
$album = $this->createAlbum('album_pii');
|
||||
$this->createReview($album, $author);
|
||||
|
||||
$this->client->request('GET', '/api/v1/reviews?limit=1');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
self::assertIsArray($payload);
|
||||
self::assertIsArray($payload['data'] ?? null);
|
||||
self::assertNotEmpty($payload['data']);
|
||||
|
||||
$first = $payload['data'][0] ?? null;
|
||||
self::assertIsArray($first);
|
||||
$authorPayload = $first['author'] ?? null;
|
||||
self::assertIsArray($authorPayload);
|
||||
self::assertArrayHasKey('id', $authorPayload);
|
||||
self::assertArrayHasKey('display_name', $authorPayload);
|
||||
self::assertArrayNotHasKey('email', $authorPayload);
|
||||
}
|
||||
|
||||
public function testApiIndexExposesHypermediaLinksAndActions(): void
|
||||
{
|
||||
$this->client->request('GET', '/api/v1');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
self::assertIsArray($payload);
|
||||
self::assertIsArray($payload['links'] ?? null);
|
||||
self::assertSame('/api/v1', $payload['links']['self'] ?? null);
|
||||
self::assertSame('/api/v1/albums', $payload['links']['albums'] ?? null);
|
||||
self::assertSame('/api/v1/reviews', $payload['links']['reviews'] ?? null);
|
||||
self::assertSame('/api/docs', $payload['links']['docs'] ?? null);
|
||||
self::assertIsArray($payload['actions'] ?? null);
|
||||
self::assertSame('/api/v1/auth/login', $payload['actions']['login']['href'] ?? null);
|
||||
self::assertSame('POST', $payload['actions']['login']['method'] ?? null);
|
||||
}
|
||||
|
||||
public function testAlbumShowIncludesHypermediaLinksAndActions(): void
|
||||
{
|
||||
$album = $this->createAlbum('album_hypermedia');
|
||||
|
||||
$this->client->request('GET', '/api/v1/albums/' . $album->getExternalId());
|
||||
$response = $this->client->getResponse();
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
self::assertIsArray($payload);
|
||||
self::assertIsArray($payload['data'] ?? null);
|
||||
self::assertSame('/api/v1/albums/album_hypermedia', $payload['data']['links']['self'] ?? null);
|
||||
self::assertSame('/api/v1/albums/album_hypermedia/reviews', $payload['data']['links']['reviews'] ?? null);
|
||||
self::assertSame('https://example.test/albums/album_hypermedia', $payload['data']['links']['open_source'] ?? null);
|
||||
self::assertSame('/api/v1/reviews', $payload['data']['actions']['create_review']['href'] ?? null);
|
||||
self::assertSame('/api/v1/albums/ingest', $payload['data']['actions']['ingest']['href'] ?? null);
|
||||
}
|
||||
|
||||
public function testAlbumReviewsEndpointIncludesHypermedia(): void
|
||||
{
|
||||
$author = $this->createUser('album-reviews-hypermedia@example.test');
|
||||
$album = $this->createAlbum('album_reviews_hypermedia');
|
||||
$review = $this->createReview($album, $author);
|
||||
|
||||
$this->client->request('GET', '/api/v1/albums/' . $album->getExternalId() . '/reviews');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
self::assertIsArray($payload);
|
||||
self::assertSame('/api/v1/albums/album_reviews_hypermedia/reviews', $payload['links']['self'] ?? null);
|
||||
self::assertSame('/api/v1/reviews', $payload['links']['reviews'] ?? null);
|
||||
self::assertSame('/api/v1/reviews', $payload['actions']['create_review']['href'] ?? null);
|
||||
self::assertSame('POST', $payload['actions']['create_review']['method'] ?? null);
|
||||
self::assertIsArray($payload['data'] ?? null);
|
||||
self::assertNotEmpty($payload['data']);
|
||||
|
||||
$first = $payload['data'][0] ?? null;
|
||||
self::assertIsArray($first);
|
||||
self::assertSame($review->getId(), $first['id'] ?? null);
|
||||
self::assertSame('/api/v1/reviews/' . $review->getId(), $first['links']['self'] ?? null);
|
||||
self::assertSame('/api/v1/albums/album_reviews_hypermedia', $first['links']['album'] ?? null);
|
||||
}
|
||||
|
||||
private function resetDatabase(): void
|
||||
{
|
||||
$allMetadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
|
||||
if ($allMetadata === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schemaTool = new SchemaTool($this->entityManager);
|
||||
$schemaTool->dropDatabase();
|
||||
$schemaTool->createSchema($allMetadata);
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
|
||||
private function createUser(string $email, string $plainPassword = 'password123'): User
|
||||
{
|
||||
/** @var UserPasswordHasherInterface $passwordHasher */
|
||||
$passwordHasher = static::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($email);
|
||||
$user->setDisplayName('Test User');
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $plainPassword));
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createAlbum(string $externalId): Album
|
||||
{
|
||||
$album = new Album();
|
||||
$album->setSource('external');
|
||||
$album->setExternalId($externalId);
|
||||
$album->setName('Functional Test Album');
|
||||
$album->setArtists(['Functional Test Artist']);
|
||||
$album->setGenres(['Rock']);
|
||||
$album->setReleaseDate('2020-01-01');
|
||||
$album->setTotalTracks(1);
|
||||
$album->setExternalUrl('https://example.test/albums/' . $externalId);
|
||||
|
||||
$track = new AlbumTrack();
|
||||
$track->setDiscNumber(1);
|
||||
$track->setTrackNumber(1);
|
||||
$track->setName('Functional Test Track');
|
||||
$track->setDurationMs(210000);
|
||||
$album->addTrack($track);
|
||||
|
||||
$this->entityManager->persist($album);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $album;
|
||||
}
|
||||
|
||||
private function createReview(Album $album, User $author): Review
|
||||
{
|
||||
$review = new Review();
|
||||
$review->setAlbum($album);
|
||||
$review->setAuthor($author);
|
||||
$review->setTitle('Excellent reference review');
|
||||
$review->setContent('This review content is comfortably longer than twenty characters.');
|
||||
$review->setRating(8);
|
||||
|
||||
$this->entityManager->persist($review);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $review;
|
||||
}
|
||||
|
||||
private function issueToken(User $user): string
|
||||
{
|
||||
/** @var JWTTokenManagerInterface $jwtTokenManager */
|
||||
$jwtTokenManager = static::getContainer()->get(JWTTokenManagerInterface::class);
|
||||
|
||||
return $jwtTokenManager->create($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function assertErrorEnvelope(int $expectedStatus, string|array $expectedCode): array
|
||||
{
|
||||
$response = $this->client->getResponse();
|
||||
self::assertSame($expectedStatus, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
self::assertIsArray($payload);
|
||||
self::assertIsArray($payload['error'] ?? null);
|
||||
if (is_array($expectedCode)) {
|
||||
self::assertContains($payload['error']['code'] ?? null, $expectedCode);
|
||||
} else {
|
||||
self::assertSame($expectedCode, $payload['error']['code'] ?? null);
|
||||
}
|
||||
self::assertSame($expectedStatus, $payload['error']['status'] ?? null);
|
||||
self::assertIsString($payload['error']['message'] ?? null);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Web;
|
||||
|
||||
use App\Entity\Album;
|
||||
use App\Entity\AlbumTrack;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
final class AlbumWorkflowRegressionTest extends WebTestCase
|
||||
{
|
||||
private KernelBrowser $client;
|
||||
private EntityManagerInterface $entityManager;
|
||||
private string $projectDir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::ensureKernelShutdown();
|
||||
$this->client = static::createClient();
|
||||
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$this->projectDir = (string) static::getContainer()->getParameter('kernel.project_dir');
|
||||
$this->resetDatabase();
|
||||
$this->ensureUploadDirectories();
|
||||
}
|
||||
|
||||
public function testAlbumEditAndDeleteRoutesWorkForUserAlbum(): void
|
||||
{
|
||||
$user = $this->createUser('album-owner@example.test');
|
||||
$album = $this->createUserAlbum($user, 'u_album_route_fix');
|
||||
|
||||
$this->client->loginUser($user);
|
||||
|
||||
$crawler = $this->client->request('GET', '/albums/' . $album->getExternalId());
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
|
||||
$this->client->request('GET', '/albums/' . $album->getExternalId() . '/edit');
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
|
||||
$token = $crawler->filter('form[action$="/albums/' . $album->getExternalId() . '/delete"] input[name="_token"]')->attr('value');
|
||||
self::assertIsString($token);
|
||||
|
||||
$this->client->request('POST', '/albums/' . $album->getExternalId() . '/delete', ['_token' => $token]);
|
||||
self::assertResponseStatusCodeSame(302);
|
||||
|
||||
$deleted = $this->entityManager->getRepository(Album::class)->findOneBy(['externalId' => $album->getExternalId()]);
|
||||
self::assertNull($deleted);
|
||||
}
|
||||
|
||||
public function testCreateAlbumWithCoverUploadDoesNotErrorAndPersistsCoverPath(): void
|
||||
{
|
||||
$user = $this->createUser('cover-upload@example.test');
|
||||
$this->client->loginUser($user);
|
||||
|
||||
$crawler = $this->client->request('GET', '/albums/new');
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
|
||||
$formToken = $crawler->filter('input[name="album[_token]"]')->attr('value');
|
||||
self::assertIsString($formToken);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'cover_');
|
||||
self::assertIsString($tmpFile);
|
||||
file_put_contents($tmpFile, base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8Xw8AAoMBgQwM0R8AAAAASUVORK5CYII=', true));
|
||||
$cover = new UploadedFile($tmpFile, 'cover.png', 'image/png', null, true);
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/albums/new',
|
||||
[
|
||||
'album' => [
|
||||
'_token' => $formToken,
|
||||
'name' => 'Cover Upload Regression',
|
||||
'artistsCsv' => 'Cover Artist',
|
||||
'genresCsv' => 'Rock',
|
||||
'releaseDate' => '2024-01-01',
|
||||
'totalTracks' => '1',
|
||||
'externalUrl' => 'https://example.test/cover-source',
|
||||
],
|
||||
'track_title' => ['Track One'],
|
||||
'track_duration' => ['3:21'],
|
||||
'track_disc' => ['1'],
|
||||
'track_number' => ['1'],
|
||||
],
|
||||
[
|
||||
'album' => [
|
||||
'coverUpload' => $cover,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
self::assertResponseStatusCodeSame(302);
|
||||
|
||||
$album = $this->entityManager->getRepository(Album::class)->findOneBy(['name' => 'Cover Upload Regression']);
|
||||
self::assertInstanceOf(Album::class, $album);
|
||||
self::assertNotNull($album->getCoverImagePath());
|
||||
self::assertStringStartsWith('/uploads/album_covers/', (string) $album->getCoverImagePath());
|
||||
self::assertFileExists($this->projectDir . '/public' . $album->getCoverImagePath());
|
||||
}
|
||||
|
||||
public function testDashboardPagesLinkToUserAlbumByExternalId(): void
|
||||
{
|
||||
$owner = $this->createUser('dashboard-owner@example.test');
|
||||
$moderator = $this->createUser('dashboard-moderator@example.test', ['ROLE_MODERATOR']);
|
||||
$album = $this->createUserAlbum($owner, 'u_dashboard_link');
|
||||
|
||||
$this->client->loginUser($owner);
|
||||
$this->client->request('GET', '/dashboard');
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
self::assertStringContainsString('/albums/' . $album->getExternalId(), (string) $this->client->getResponse()->getContent());
|
||||
|
||||
$this->client->loginUser($moderator);
|
||||
$this->client->request('GET', '/admin/dashboard');
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
self::assertStringContainsString('/albums/' . $album->getExternalId(), (string) $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
private function ensureUploadDirectories(): void
|
||||
{
|
||||
$base = $this->projectDir . '/public/uploads';
|
||||
@mkdir($base . '/album_covers', 0777, true);
|
||||
@mkdir($base . '/avatars', 0777, true);
|
||||
}
|
||||
|
||||
private function resetDatabase(): void
|
||||
{
|
||||
$allMetadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
|
||||
if ($allMetadata === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schemaTool = new SchemaTool($this->entityManager);
|
||||
$schemaTool->dropDatabase();
|
||||
$schemaTool->createSchema($allMetadata);
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
private function createUser(string $email, array $roles = []): User
|
||||
{
|
||||
/** @var UserPasswordHasherInterface $passwordHasher */
|
||||
$passwordHasher = static::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($email);
|
||||
$user->setDisplayName('Regression User');
|
||||
$user->setRoles($roles);
|
||||
$user->setPassword($passwordHasher->hashPassword($user, 'password123'));
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createUserAlbum(User $owner, string $externalId): Album
|
||||
{
|
||||
$album = new Album();
|
||||
$album->setSource('user');
|
||||
$album->setExternalId($externalId);
|
||||
$album->setName('User Album ' . $externalId);
|
||||
$album->setArtists(['Regression Artist']);
|
||||
$album->setGenres(['Rock']);
|
||||
$album->setReleaseDate('2024-01-01');
|
||||
$album->setTotalTracks(1);
|
||||
$album->setOwner($owner);
|
||||
|
||||
$track = new AlbumTrack();
|
||||
$track->setDiscNumber(1);
|
||||
$track->setTrackNumber(1);
|
||||
$track->setName('Regression Track');
|
||||
$track->setDurationMs(180000);
|
||||
$album->addTrack($track);
|
||||
|
||||
$this->entityManager->persist($album);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $album;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Service;
|
||||
|
||||
use App\Service\ExternalAlbumClient;
|
||||
use App\Service\ExternalProviderManager;
|
||||
use App\Service\LastFmClient;
|
||||
use App\Service\MusicBrainzClient;
|
||||
use App\Service\TidalClient;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ExternalAlbumClientTest extends TestCase
|
||||
{
|
||||
public function testUnknownProviderFallsBackToLastFm(): void
|
||||
{
|
||||
$lastFm = $this->createMock(LastFmClient::class);
|
||||
$musicBrainz = $this->createMock(MusicBrainzClient::class);
|
||||
$tidal = $this->createMock(TidalClient::class);
|
||||
$providerManager = $this->createMock(ExternalProviderManager::class);
|
||||
|
||||
$providerManager->expects(self::atLeastOnce())
|
||||
->method('getEffectiveProvider')
|
||||
->willReturn('unknown-provider');
|
||||
|
||||
$lastFm->expects(self::once())
|
||||
->method('searchAlbums')
|
||||
->with('radiohead', 5, 1)
|
||||
->willReturn(['albums' => ['items' => []]]);
|
||||
$lastFm->expects(self::once())
|
||||
->method('getProviderName')
|
||||
->willReturn('lastfm');
|
||||
|
||||
$musicBrainz->expects(self::never())->method('searchAlbums');
|
||||
$tidal->expects(self::never())->method('searchAlbums');
|
||||
|
||||
$client = new ExternalAlbumClient($lastFm, $musicBrainz, $tidal, $providerManager);
|
||||
$result = $client->searchAlbums('radiohead', 5, 1);
|
||||
|
||||
self::assertSame(['albums' => ['items' => []]], $result);
|
||||
self::assertSame('lastfm', $client->getProviderName());
|
||||
}
|
||||
|
||||
public function testMusicBrainzProviderRoutesCallsToMusicBrainzClient(): void
|
||||
{
|
||||
$lastFm = $this->createMock(LastFmClient::class);
|
||||
$musicBrainz = $this->createMock(MusicBrainzClient::class);
|
||||
$tidal = $this->createMock(TidalClient::class);
|
||||
$providerManager = $this->createMock(ExternalProviderManager::class);
|
||||
|
||||
$providerManager->expects(self::atLeastOnce())
|
||||
->method('getEffectiveProvider')
|
||||
->willReturn('musicbrainz');
|
||||
|
||||
$musicBrainz->expects(self::once())
|
||||
->method('searchAlbums')
|
||||
->with('tool', 3, 2)
|
||||
->willReturn([
|
||||
'albums' => [
|
||||
'items' => [
|
||||
['id' => 'mbid:abc', 'name' => 'Lateralus'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$musicBrainz->expects(self::once())
|
||||
->method('getProviderName')
|
||||
->willReturn('musicbrainz');
|
||||
|
||||
$lastFm->expects(self::never())->method('searchAlbums');
|
||||
$tidal->expects(self::never())->method('searchAlbums');
|
||||
|
||||
$client = new ExternalAlbumClient($lastFm, $musicBrainz, $tidal, $providerManager);
|
||||
$result = $client->searchAlbums('tool', 3, 2);
|
||||
|
||||
self::assertSame('mbid:abc', $result['albums']['items'][0]['id'] ?? null);
|
||||
self::assertSame('musicbrainz', $client->getProviderName());
|
||||
}
|
||||
|
||||
public function testTidalProviderRoutesCallsToTidalClient(): void
|
||||
{
|
||||
$lastFm = $this->createMock(LastFmClient::class);
|
||||
$musicBrainz = $this->createMock(MusicBrainzClient::class);
|
||||
$tidal = $this->createMock(TidalClient::class);
|
||||
$providerManager = $this->createMock(ExternalProviderManager::class);
|
||||
|
||||
$providerManager->expects(self::atLeastOnce())
|
||||
->method('getEffectiveProvider')
|
||||
->willReturn('tidal');
|
||||
|
||||
$tidal->expects(self::once())
|
||||
->method('searchAlbums')
|
||||
->with('kanye', 4, 1)
|
||||
->willReturn([
|
||||
'albums' => [
|
||||
'items' => [
|
||||
['id' => 'tidal:123', 'name' => 'The College Dropout'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$tidal->expects(self::once())
|
||||
->method('getProviderName')
|
||||
->willReturn('tidal');
|
||||
|
||||
$lastFm->expects(self::never())->method('searchAlbums');
|
||||
$musicBrainz->expects(self::never())->method('searchAlbums');
|
||||
|
||||
$client = new ExternalAlbumClient($lastFm, $musicBrainz, $tidal, $providerManager);
|
||||
$result = $client->searchAlbums('kanye', 4, 1);
|
||||
|
||||
self::assertSame('tidal:123', $result['albums']['items'][0]['id'] ?? null);
|
||||
self::assertSame('tidal', $client->getProviderName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Service;
|
||||
|
||||
use App\Repository\SettingRepository;
|
||||
use App\Service\MusicBrainzClient;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
final class MusicBrainzClientTest extends TestCase
|
||||
{
|
||||
public function testDetailedAlbumUsesCanonicalMusicBrainzUrlForOpenSourceLink(): void
|
||||
{
|
||||
$releaseId = '78eb2077-b86d-4382-9ce7-38f9260316f0';
|
||||
|
||||
$httpClient = $this->createMock(ClientInterface::class);
|
||||
$httpClient->expects(self::once())
|
||||
->method('request')
|
||||
->with(
|
||||
'GET',
|
||||
'https://musicbrainz.org/ws/2/release/' . $releaseId,
|
||||
self::callback(static function (array $options): bool {
|
||||
$query = $options['query'] ?? [];
|
||||
return ($query['fmt'] ?? null) === 'json';
|
||||
})
|
||||
)
|
||||
->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode([
|
||||
'id' => $releaseId,
|
||||
'title' => 'Reference Album',
|
||||
'artist-credit' => [
|
||||
['name' => 'Reference Artist'],
|
||||
],
|
||||
'media' => [[
|
||||
'position' => 1,
|
||||
'tracks' => [[
|
||||
'id' => 'track-1',
|
||||
'number' => '1',
|
||||
'title' => 'Reference Track',
|
||||
'length' => 180000,
|
||||
]],
|
||||
]],
|
||||
// External storefront URL should not be used as the "Open Source" link.
|
||||
'relations' => [[
|
||||
'url' => ['resource' => 'https://store.example.test/reference-album'],
|
||||
]],
|
||||
], JSON_THROW_ON_ERROR)));
|
||||
|
||||
$settings = $this->createMock(SettingRepository::class);
|
||||
$cache = new ArrayAdapter();
|
||||
|
||||
$client = new MusicBrainzClient($httpClient, $cache, $settings);
|
||||
$album = $client->getAlbumWithTracks('mbid:' . $releaseId);
|
||||
|
||||
self::assertIsArray($album);
|
||||
self::assertSame(
|
||||
'https://musicbrainz.org/release/' . $releaseId,
|
||||
$album['external_urls']['external'] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Service;
|
||||
|
||||
use App\Repository\SettingRepository;
|
||||
use App\Service\TidalClient;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
final class TidalClientTest extends TestCase
|
||||
{
|
||||
public function testIsConfiguredRequiresClientIdAndSecret(): void
|
||||
{
|
||||
$http = $this->createMock(ClientInterface::class);
|
||||
$settings = $this->createMock(SettingRepository::class);
|
||||
$cache = new ArrayAdapter();
|
||||
|
||||
$client = new TidalClient($http, $cache, $settings, 'client-id-only', null, 'US');
|
||||
|
||||
self::assertFalse($client->isConfigured());
|
||||
}
|
||||
|
||||
public function testSearchAlbumsUsesClientCredentialsAndNormalizesPayload(): void
|
||||
{
|
||||
$http = $this->createMock(ClientInterface::class);
|
||||
$settings = $this->createMock(SettingRepository::class);
|
||||
$cache = new ArrayAdapter();
|
||||
|
||||
$call = 0;
|
||||
$http->expects(self::exactly(2))
|
||||
->method('request')
|
||||
->willReturnCallback(function (string $method, string $uri, array $options) use (&$call) {
|
||||
$call++;
|
||||
if ($call === 1) {
|
||||
self::assertSame('POST', $method);
|
||||
self::assertSame('https://auth.tidal.com/v1/oauth2/token', $uri);
|
||||
self::assertSame('client_credentials', $options['form_params']['grant_type'] ?? null);
|
||||
self::assertSame('tidal-client-id', $options['form_params']['client_id'] ?? null);
|
||||
self::assertSame('tidal-client-secret', $options['form_params']['client_secret'] ?? null);
|
||||
|
||||
return new Response(200, ['Content-Type' => 'application/json'], json_encode([
|
||||
'access_token' => 'token-abc',
|
||||
'expires_in' => 3600,
|
||||
'token_type' => 'Bearer',
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
self::assertSame('GET', $method);
|
||||
self::assertStringContainsString('/searchResults/kanye%20west/relationships/albums', $uri);
|
||||
self::assertSame('US', $options['query']['countryCode'] ?? null);
|
||||
self::assertSame('albums', $options['query']['include'] ?? null);
|
||||
self::assertSame('Bearer token-abc', $options['headers']['Authorization'] ?? null);
|
||||
|
||||
return new Response(200, ['Content-Type' => 'application/vnd.api+json'], json_encode([
|
||||
'data' => [
|
||||
['type' => 'albums', 'id' => '251380836'],
|
||||
],
|
||||
'included' => [
|
||||
[
|
||||
'type' => 'albums',
|
||||
'id' => '251380836',
|
||||
'attributes' => [
|
||||
'title' => '4:44',
|
||||
'releaseDate' => '2017-06-30',
|
||||
'numberOfItems' => 10,
|
||||
'externalLinks' => [
|
||||
[
|
||||
'href' => 'https://tidal.com/browse/album/251380836',
|
||||
'meta' => ['type' => 'TIDAL_SHARING'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'relationships' => [
|
||||
'artists' => [
|
||||
'data' => [
|
||||
['type' => 'artists', 'id' => 'artist-1'],
|
||||
],
|
||||
],
|
||||
'genres' => [
|
||||
'data' => [
|
||||
['type' => 'genres', 'id' => 'genre-1'],
|
||||
],
|
||||
],
|
||||
'coverArt' => [
|
||||
'data' => [
|
||||
['type' => 'artworks', 'id' => 'art-1'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'artists',
|
||||
'id' => 'artist-1',
|
||||
'attributes' => ['name' => 'JAY-Z'],
|
||||
],
|
||||
[
|
||||
'type' => 'genres',
|
||||
'id' => 'genre-1',
|
||||
'attributes' => ['genreName' => 'hip hop'],
|
||||
],
|
||||
[
|
||||
'type' => 'artworks',
|
||||
'id' => 'art-1',
|
||||
'attributes' => [
|
||||
'files' => [
|
||||
['href' => 'https://cdn.example.test/160.jpg', 'meta' => ['width' => 160, 'height' => 160]],
|
||||
['href' => 'https://cdn.example.test/320.jpg', 'meta' => ['width' => 320, 'height' => 320]],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'links' => ['self' => '/searchResults/kanye west/relationships/albums'],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
});
|
||||
|
||||
$client = new TidalClient($http, $cache, $settings, 'tidal-client-id', 'tidal-client-secret', 'US');
|
||||
$result = $client->searchAlbums('kanye west', 5, 1);
|
||||
|
||||
$items = $result['albums']['items'] ?? [];
|
||||
self::assertCount(1, $items);
|
||||
self::assertSame('tidal:251380836', $items[0]['id'] ?? null);
|
||||
self::assertSame('4:44', $items[0]['name'] ?? null);
|
||||
self::assertSame('JAY-Z', $items[0]['artists'][0]['name'] ?? null);
|
||||
self::assertSame('Hip Hop', $items[0]['genres'][0] ?? null);
|
||||
self::assertSame('2017-06-30', $items[0]['release_date'] ?? null);
|
||||
self::assertSame(10, $items[0]['total_tracks'] ?? null);
|
||||
self::assertSame('https://cdn.example.test/320.jpg', $items[0]['images'][0]['url'] ?? null);
|
||||
self::assertSame('https://tidal.com/browse/album/251380836', $items[0]['external_urls']['external'] ?? null);
|
||||
}
|
||||
|
||||
public function testGetAlbumWithTracksNormalizesTracksAndCoverArt(): void
|
||||
{
|
||||
$http = $this->createMock(ClientInterface::class);
|
||||
$settings = $this->createMock(SettingRepository::class);
|
||||
$cache = new ArrayAdapter();
|
||||
|
||||
$call = 0;
|
||||
$http->expects(self::exactly(2))
|
||||
->method('request')
|
||||
->willReturnCallback(function (string $method, string $uri, array $options) use (&$call) {
|
||||
$call++;
|
||||
if ($call === 1) {
|
||||
return new Response(200, ['Content-Type' => 'application/json'], json_encode([
|
||||
'access_token' => 'token-xyz',
|
||||
'expires_in' => 3600,
|
||||
'token_type' => 'Bearer',
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
self::assertSame('GET', $method);
|
||||
self::assertStringContainsString('/albums/251380836', $uri);
|
||||
self::assertSame('artists,coverArt,genres,items', $options['query']['include'] ?? null);
|
||||
self::assertSame('US', $options['query']['countryCode'] ?? null);
|
||||
|
||||
return new Response(200, ['Content-Type' => 'application/vnd.api+json'], json_encode([
|
||||
'data' => [
|
||||
'type' => 'albums',
|
||||
'id' => '251380836',
|
||||
'attributes' => [
|
||||
'title' => '4:44',
|
||||
'releaseDate' => '2017-06-30',
|
||||
'numberOfItems' => 2,
|
||||
'externalLinks' => [
|
||||
[
|
||||
'href' => 'https://tidal.com/browse/album/251380836',
|
||||
'meta' => ['type' => 'TIDAL_SHARING'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'relationships' => [
|
||||
'artists' => [
|
||||
'data' => [
|
||||
['type' => 'artists', 'id' => 'artist-1'],
|
||||
],
|
||||
],
|
||||
'genres' => [
|
||||
'data' => [
|
||||
['type' => 'genres', 'id' => 'genre-1'],
|
||||
],
|
||||
],
|
||||
'coverArt' => [
|
||||
'data' => [
|
||||
['type' => 'artworks', 'id' => 'art-1'],
|
||||
],
|
||||
],
|
||||
'items' => [
|
||||
'data' => [
|
||||
['type' => 'tracks', 'id' => 'track-1', 'meta' => ['volumeNumber' => 1, 'trackNumber' => 1]],
|
||||
['type' => 'tracks', 'id' => 'track-2', 'meta' => ['volumeNumber' => 1, 'trackNumber' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'included' => [
|
||||
['type' => 'artists', 'id' => 'artist-1', 'attributes' => ['name' => 'JAY-Z']],
|
||||
['type' => 'genres', 'id' => 'genre-1', 'attributes' => ['genreName' => 'hip hop']],
|
||||
[
|
||||
'type' => 'artworks',
|
||||
'id' => 'art-1',
|
||||
'attributes' => [
|
||||
'files' => [
|
||||
['href' => 'https://cdn.example.test/640.jpg', 'meta' => ['width' => 640, 'height' => 640]],
|
||||
],
|
||||
],
|
||||
],
|
||||
['type' => 'tracks', 'id' => 'track-1', 'attributes' => ['title' => 'Kill Jay Z', 'duration' => 'PT3M25S']],
|
||||
['type' => 'tracks', 'id' => 'track-2', 'attributes' => ['title' => 'The Story of O.J.', 'duration' => 'PT3M52S']],
|
||||
],
|
||||
'links' => ['self' => '/albums/251380836'],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
});
|
||||
|
||||
$client = new TidalClient($http, $cache, $settings, 'tidal-client-id', 'tidal-client-secret', 'US');
|
||||
$album = $client->getAlbumWithTracks('tidal:251380836');
|
||||
|
||||
self::assertIsArray($album);
|
||||
self::assertSame('tidal:251380836', $album['id'] ?? null);
|
||||
self::assertSame('4:44', $album['name'] ?? null);
|
||||
self::assertSame('JAY-Z', $album['artists'][0]['name'] ?? null);
|
||||
self::assertSame('Hip Hop', $album['genres'][0] ?? null);
|
||||
self::assertSame('2017-06-30', $album['release_date'] ?? null);
|
||||
self::assertSame('https://cdn.example.test/640.jpg', $album['images'][0]['url'] ?? null);
|
||||
self::assertSame(2, $album['total_tracks'] ?? null);
|
||||
self::assertSame(2, $album['tracks']['total'] ?? null);
|
||||
self::assertSame('Kill Jay Z', $album['tracks']['items'][0]['name'] ?? null);
|
||||
self::assertSame(205000, $album['tracks']['items'][0]['duration_ms'] ?? null);
|
||||
self::assertSame(1, $album['tracks']['items'][0]['track_number'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
if (class_exists(Dotenv::class)) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__) . '/.env');
|
||||
}
|
||||
|
||||
$_ENV['APP_ENV'] = $_SERVER['APP_ENV'] = 'test';
|
||||
$_ENV['APP_DEBUG'] = $_SERVER['APP_DEBUG'] = '1';
|
||||
$_ENV['APP_SECRET'] = $_SERVER['APP_SECRET'] = 'test-secret';
|
||||
$_ENV['KERNEL_CLASS'] = $_SERVER['KERNEL_CLASS'] = 'App\\Kernel';
|
||||
$_ENV['DATABASE_DRIVER'] = $_SERVER['DATABASE_DRIVER'] = 'sqlite';
|
||||
$_ENV['DATABASE_SQLITE_PATH'] = $_SERVER['DATABASE_SQLITE_PATH'] = '/tmp/tonehaus_test.sqlite';
|
||||
$_ENV['JWT_PASSPHRASE'] = $_SERVER['JWT_PASSPHRASE'] = 'test';
|
||||
|
||||
putenv('APP_ENV=test');
|
||||
putenv('APP_DEBUG=1');
|
||||
putenv('APP_SECRET=test-secret');
|
||||
putenv('KERNEL_CLASS=App\\Kernel');
|
||||
putenv('DATABASE_DRIVER=sqlite');
|
||||
putenv('DATABASE_SQLITE_PATH=/tmp/tonehaus_test.sqlite');
|
||||
putenv('JWT_PASSPHRASE=test');
|
||||
Reference in new issue
Block a user