Web app: new entities (Image, RenderedAsset, SharedImage, Token, DeviceImageHistory), enums, repositories, controllers, message handlers, migrations, tests, frontend upload/library/sticker UI, Vue components. Firmware: EPD background screen binaries + gen scripts, setup_bg header. Infra: ddev config, test bundle, gitignore coverage dir. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Controller;
|
||||
|
||||
use App\Entity\Device;
|
||||
use App\Entity\Image;
|
||||
use App\Entity\RenderedAsset;
|
||||
use App\Entity\SharedImage;
|
||||
use App\Enum\DeviceModel;
|
||||
use App\Enum\Orientation;
|
||||
use App\Enum\RenderStatus;
|
||||
use App\Enum\SharedImageStatus;
|
||||
use App\Service\RotationService;
|
||||
use App\Tests\Functional\AppWebTestCase;
|
||||
use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport;
|
||||
|
||||
class SharedImageApiControllerTest extends AppWebTestCase
|
||||
{
|
||||
private function makeImage($user): Image
|
||||
{
|
||||
$image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
|
||||
$this->em()->persist($image);
|
||||
return $image;
|
||||
}
|
||||
|
||||
private function makeShared(Image $image, $recipient, $sharedBy, SharedImageStatus $status = SharedImageStatus::Pending): SharedImage
|
||||
{
|
||||
$shared = new SharedImage($image, $recipient, $sharedBy);
|
||||
$shared->setStatus($status);
|
||||
$this->em()->persist($shared);
|
||||
return $shared;
|
||||
}
|
||||
|
||||
public function test_list_returns_paginated_result_for_recipient(): void
|
||||
{
|
||||
$sender = $this->createUser('sender@example.com');
|
||||
$recipient = $this->createUser('recip@example.com');
|
||||
$image = $this->makeImage($sender);
|
||||
$this->makeShared($image, $recipient, $sender);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('GET', '/api/shared-images');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertArrayHasKey('items', $data);
|
||||
$this->assertArrayHasKey('total', $data);
|
||||
$this->assertCount(1, $data['items']);
|
||||
$this->assertSame($image->getId(), $data['items'][0]['imageId']);
|
||||
}
|
||||
|
||||
public function test_list_filters_by_status_pending(): void
|
||||
{
|
||||
$sender = $this->createUser('senderf@example.com');
|
||||
$recipient = $this->createUser('recipf@example.com');
|
||||
$img1 = $this->makeImage($sender);
|
||||
$img2 = $this->makeImage($sender);
|
||||
$this->makeShared($img1, $recipient, $sender, SharedImageStatus::Pending);
|
||||
$this->makeShared($img2, $recipient, $sender, SharedImageStatus::Approved);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('GET', '/api/shared-images?status=pending');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertCount(1, $data['items']);
|
||||
$this->assertSame('pending', $data['items'][0]['status']);
|
||||
}
|
||||
|
||||
public function test_approve_sets_status_approved(): void
|
||||
{
|
||||
$sender = $this->createUser('sendera@example.com');
|
||||
$recipient = $this->createUser('recipa@example.com');
|
||||
$image = $this->makeImage($sender);
|
||||
$shared = $this->makeShared($image, $recipient, $sender);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('POST', '/api/shared-images/' . $shared->getId() . '/approve', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['deviceIds' => []]));
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame('approved', $data['status']);
|
||||
|
||||
$sharedId = $shared->getId();
|
||||
$this->em()->clear();
|
||||
$shared = $this->em()->find(SharedImage::class, $sharedId);
|
||||
$this->assertSame(SharedImageStatus::Approved, $shared->getStatus());
|
||||
|
||||
// SH-03: verify that render messages were dispatched
|
||||
/** @var \Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport $transport */
|
||||
$transport = static::getContainer()->get('messenger.transport.image_processing');
|
||||
$this->assertGreaterThan(0, count($transport->get()), 'Expected RenderImageMessage(s) to be dispatched after approve');
|
||||
}
|
||||
|
||||
public function test_decline_sets_status_declined(): void
|
||||
{
|
||||
$sender = $this->createUser('senderd@example.com');
|
||||
$recipient = $this->createUser('recipd@example.com');
|
||||
$image = $this->makeImage($sender);
|
||||
$shared = $this->makeShared($image, $recipient, $sender);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('POST', '/api/shared-images/' . $shared->getId() . '/decline');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame('declined', $data['status']);
|
||||
|
||||
$sharedId = $shared->getId();
|
||||
$this->em()->clear();
|
||||
$shared = $this->em()->find(SharedImage::class, $sharedId);
|
||||
$this->assertSame(SharedImageStatus::Declined, $shared->getStatus());
|
||||
}
|
||||
|
||||
public function test_approve_on_another_users_shared_returns_404(): void
|
||||
{
|
||||
$sender = $this->createUser('senderx@example.com');
|
||||
$recipient = $this->createUser('recipx@example.com');
|
||||
$attacker = $this->createUser('attack@example.com');
|
||||
$image = $this->makeImage($sender);
|
||||
$shared = $this->makeShared($image, $recipient, $sender);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($attacker);
|
||||
$client->request('POST', '/api/shared-images/' . $shared->getId() . '/approve', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['deviceIds' => []]));
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function test_pending_count_returns_count(): void
|
||||
{
|
||||
$sender = $this->createUser('pcountsend@example.com');
|
||||
$recipient = $this->createUser('pcountrecip@example.com');
|
||||
$img1 = $this->makeImage($sender);
|
||||
$img2 = $this->makeImage($sender);
|
||||
$this->makeShared($img1, $recipient, $sender, SharedImageStatus::Pending);
|
||||
$this->makeShared($img2, $recipient, $sender, SharedImageStatus::Approved);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('GET', '/api/shared-images/pending-count');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data['count']);
|
||||
}
|
||||
|
||||
public function test_decline_previously_approved_revokes_device_approvals(): void
|
||||
{
|
||||
$sender = $this->createUser('declrevoke_send@example.com');
|
||||
$recipient = $this->createUser('declrevoke_recip@example.com');
|
||||
|
||||
$device = new Device();
|
||||
$device->setMac('AA:BB:CC:DD:99:01');
|
||||
$device->setName('Revoke Frame');
|
||||
$device->setUser($recipient);
|
||||
$this->em()->persist($device);
|
||||
|
||||
$image = $this->makeImage($sender);
|
||||
$image->approveForDevice($device);
|
||||
$shared = $this->makeShared($image, $recipient, $sender, SharedImageStatus::Approved);
|
||||
$this->em()->flush();
|
||||
|
||||
$imageId = $image->getId();
|
||||
$deviceId = $device->getId();
|
||||
$sharedId = $shared->getId();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('POST', '/api/shared-images/' . $sharedId . '/decline');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->em()->clear();
|
||||
$imageReloaded = $this->em()->find(\App\Entity\Image::class, $imageId);
|
||||
$deviceReloaded = $this->em()->find(Device::class, $deviceId);
|
||||
$this->assertFalse($imageReloaded->isApprovedForDevice($deviceReloaded));
|
||||
}
|
||||
|
||||
public function test_approve_with_unowned_device_id_skips_it(): void
|
||||
{
|
||||
$sender = $this->createUser('senderunown@example.com');
|
||||
$recipient = $this->createUser('recipunown@example.com');
|
||||
$other = $this->createUser('otherunown@example.com');
|
||||
$image = $this->makeImage($sender);
|
||||
$shared = $this->makeShared($image, $recipient, $sender);
|
||||
|
||||
$otherDevice = new Device();
|
||||
$otherDevice->setMac('AA:BB:CC:DD:99:88');
|
||||
$otherDevice->setName('Other Frame');
|
||||
$otherDevice->setUser($other);
|
||||
$this->em()->persist($otherDevice);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('POST', '/api/shared-images/' . $shared->getId() . '/approve', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['deviceIds' => [$otherDevice->getId()]]));
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->em()->clear();
|
||||
$reloaded = $this->em()->find(SharedImage::class, $shared->getId());
|
||||
$this->assertSame(SharedImageStatus::Approved, $reloaded->getStatus());
|
||||
}
|
||||
|
||||
public function test_decline_not_found_returns_404(): void
|
||||
{
|
||||
$user = $this->createUser('declnotfound@example.com');
|
||||
$client = $this->loginAs($user);
|
||||
|
||||
$client->request('POST', '/api/shared-images/99999/decline');
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* SH-06: After approve + pre-existing ready RenderedAsset, image appears in rotation pool.
|
||||
*
|
||||
* The approve endpoint dispatches RenderImageMessage but does NOT create RenderedAssets itself.
|
||||
* We pre-create a Ready RenderedAsset before the approve call, then verify that after approval
|
||||
* the image is approved for the device and RotationService::advance() returns it.
|
||||
*/
|
||||
public function test_approved_image_with_ready_asset_appears_in_rotation(): void
|
||||
{
|
||||
$sender = $this->createUser('sendersh6@example.com');
|
||||
$recipient = $this->createUser('recipsh6@example.com');
|
||||
|
||||
// Create a device owned by the recipient
|
||||
$device = new Device();
|
||||
$device->setMac('AA:BB:CC:DD:EE:06')->setName('Test Frame');
|
||||
$device->setUser($recipient);
|
||||
$this->em()->persist($device);
|
||||
|
||||
// Create the shared image
|
||||
$image = $this->makeImage($sender);
|
||||
$shared = $this->makeShared($image, $recipient, $sender);
|
||||
|
||||
// Pre-create a Ready RenderedAsset for the device's model+orientation
|
||||
$asset = (new RenderedAsset())
|
||||
->setImage($image)
|
||||
->setDeviceModel($device->getModel())
|
||||
->setOrientation($device->getOrientation())
|
||||
->setStatus(RenderStatus::Ready);
|
||||
$this->em()->persist($asset);
|
||||
|
||||
$this->em()->flush();
|
||||
|
||||
// Approve with the device ID so the image is approved for this device
|
||||
$client = $this->loginAs($recipient);
|
||||
$client->request('POST', '/api/shared-images/' . $shared->getId() . '/approve', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['deviceIds' => [$device->getId()]]));
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Reload image and assert device approval
|
||||
$this->em()->clear();
|
||||
$imageReloaded = $this->em()->find(Image::class, $image->getId());
|
||||
$deviceReloaded = $this->em()->find(Device::class, $device->getId());
|
||||
$this->assertTrue($imageReloaded->isApprovedForDevice($deviceReloaded), 'Image should be approved for device');
|
||||
|
||||
// Assert image appears in the rotation pool via RotationService::advance()
|
||||
$rotationService = static::getContainer()->get(RotationService::class);
|
||||
$next = $rotationService->advance($deviceReloaded);
|
||||
$this->assertNotNull($next, 'RotationService::advance() should return the approved image');
|
||||
$this->assertSame($imageReloaded->getId(), $next->getId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user