Files
football2801 4002ff9fbf
CI / test (push) Has been cancelled
chore: stage all in-progress work before repo split
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>
2026-05-06 12:11:31 -04:00

282 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Functional\Controller;
use App\Entity\Device;
use App\Entity\Image;
use App\Enum\DeviceModel;
use App\Enum\Orientation;
use App\Tests\Functional\AppWebTestCase;
class DeviceApiControllerTest extends AppWebTestCase
{
private function makeDevice(string $mac, $user): Device
{
$device = new Device();
$device->setMac($mac);
$device->setName('Frame ' . $mac);
$device->setUser($user);
$device->setModel(DeviceModel::V1);
$device->setOrientation(Orientation::Landscape);
$this->em()->persist($device);
$this->em()->flush();
return $device;
}
private function makeImage($user): Image
{
$image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
$this->em()->persist($image);
$this->em()->flush();
return $image;
}
public function test_list_returns_own_devices(): void
{
$user = $this->createUser('list@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A1', $user);
$client = $this->loginAs($user);
$client->request('GET', '/api/devices');
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertCount(1, $data);
$this->assertSame($device->getMac(), $data[0]['mac']);
}
public function test_list_unauthenticated_returns_redirect(): void
{
$this->client->request('GET', '/api/devices');
// form_login firewall redirects unauthenticated requests to /login
$this->assertResponseRedirects('/login');
}
public function test_patch_updates_name_orientation_and_interval(): void
{
$user = $this->createUser('patch@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A2', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'name' => 'New Name',
'orientation' => 'portrait',
'rotationIntervalMinutes' => 30,
]));
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame('New Name', $data['name']);
$this->assertSame('portrait', $data['orientation']);
$this->assertSame(30, $data['rotationIntervalMinutes']);
}
public function test_patch_wrong_users_device_returns_404(): void
{
$owner = $this->createUser('owner@example.com');
$other = $this->createUser('other@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A3', $owner);
$client = $this->loginAs($other);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['name' => 'Hack']));
$this->assertResponseStatusCodeSame(404);
}
public function test_put_lock_sets_locked_image_id(): void
{
$user = $this->createUser('lock@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A4', $user);
$image = $this->makeImage($user);
$image->approveForDevice($device);
$this->em()->flush();
$client = $this->loginAs($user);
$client->request('PUT', '/api/devices/' . $device->getId() . '/lock', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['imageId' => $image->getId()]));
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame($image->getId(), $data['lockedImageId']);
}
// A-06: PUT /lock with image not approved for the device → 422
public function test_put_lock_unapproved_image_returns_422(): void
{
$user = $this->createUser('lock422@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A8', $user);
$image = $this->makeImage($user);
// Image is owned by user but NOT approved for this device
$client = $this->loginAs($user);
$client->request('PUT', '/api/devices/' . $device->getId() . '/lock', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['imageId' => $image->getId()]));
$this->assertResponseStatusCodeSame(422);
}
public function test_put_lock_image_not_owned_by_user_returns_404(): void
{
$user1 = $this->createUser('lockown1@example.com');
$user2 = $this->createUser('lockown2@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A5', $user1);
$image = $this->makeImage($user2);
$client = $this->loginAs($user1);
$client->request('PUT', '/api/devices/' . $device->getId() . '/lock', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['imageId' => $image->getId()]));
$this->assertResponseStatusCodeSame(404);
}
public function test_delete_lock_clears_locked_image_id(): void
{
$user = $this->createUser('unlock@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A6', $user);
$image = $this->makeImage($user);
$device->setLockedImage($image);
$this->em()->flush();
$client = $this->loginAs($user);
$client->request('DELETE', '/api/devices/' . $device->getId() . '/lock');
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertNull($data['lockedImageId']);
}
public function test_put_lock_wrong_users_device_returns_404(): void
{
$owner = $this->createUser('lockwrong1@example.com');
$other = $this->createUser('lockwrong2@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:A7', $owner);
$image = $this->makeImage($other);
$client = $this->loginAs($other);
$client->request('PUT', '/api/devices/' . $device->getId() . '/lock', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['imageId' => $image->getId()]));
$this->assertResponseStatusCodeSame(404);
}
public function test_patch_empty_name_returns_422(): void
{
$user = $this->createUser('patchname@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:B1', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['name' => '']));
$this->assertResponseStatusCodeSame(422);
}
public function test_patch_invalid_orientation_returns_422(): void
{
$user = $this->createUser('patchorient@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:B2', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['orientation' => 'diagonal']));
$this->assertResponseStatusCodeSame(422);
}
public function test_patch_invalid_timezone_returns_422(): void
{
$user = $this->createUser('patchtzone@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:B3', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['timezone' => 'Not/Real']));
$this->assertResponseStatusCodeSame(422);
}
public function test_patch_sets_wake_hour(): void
{
$user = $this->createUser('patchwake@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:B4', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['wakeHour' => 8]));
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame(8, $data['wakeHour']);
}
public function test_patch_sets_uniqueness_window(): void
{
$user = $this->createUser('patchuniq@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:B5', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['uniquenessWindow' => 14]));
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame(14, $data['uniquenessWindow']);
}
public function test_patch_sets_valid_timezone(): void
{
$user = $this->createUser('patchtzvld@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:C1', $user);
$client = $this->loginAs($user);
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['timezone' => 'America/New_York']));
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame('America/New_York', $data['timezone']);
}
public function test_lock_with_no_image_id_returns_422(): void
{
$user = $this->createUser('locknoimgid@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:C2', $user);
$client = $this->loginAs($user);
$client->request('PUT', '/api/devices/' . $device->getId() . '/lock', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([]));
$this->assertResponseStatusCodeSame(422);
}
public function test_unlock_wrong_users_device_returns_404(): void
{
$owner = $this->createUser('unlockown@example.com');
$other = $this->createUser('unlockoth@example.com');
$device = $this->makeDevice('AA:BB:CC:DD:EE:C3', $owner);
$client = $this->loginAs($other);
$client->request('DELETE', '/api/devices/' . $device->getId() . '/lock');
$this->assertResponseStatusCodeSame(404);
}
}