dbudwin/RoboHome-Web

View on GitHub
tests/Unit/Controller/API/DevicesControllerTest.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php
 
namespace Tests\Unit\Controller\API;
 
use App\Device;
use App\DeviceActionInfo\IDeviceActionInfoBroker;
use App\Http\Globals\DeviceActions;
use App\Repositories\IDeviceRepository;
use App\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\TestResponse;
use Laravel\Passport\Passport;
use Mockery;
use ReflectionClass;
use Tests\Unit\Controller\Common\DevicesControllerTestCase;
use Webpatser\Uuid\Uuid;
 
`DevicesControllerTest` has 22 functions (exceeds 20 allowed). Consider refactoring.
class DevicesControllerTest extends DevicesControllerTestCase
{
private $mockDeviceRepository;
private $mockUser;
private $messageId;
 
public function setUp(): void
{
parent::setUp();
 
$this->mockDeviceRepository = Mockery::mock(IDeviceRepository::class);
$this->mockUser = $this->createMockUser();
$this->messageId = self::$faker->uuid();
 
$this->app->instance(IDeviceRepository::class, $this->mockDeviceRepository);
}
 
public function testIndex_GivenUserExistsWithNoDevices_ReturnsJsonResponse(): void
{
$this->mockUser->shouldReceive('getAttribute')->with('devices')->once()->andReturn([]);
 
$response = $this->callDevices();
 
$this->assertDiscoverAppliancesResponseWithoutDevice($response, 'DiscoverAppliancesResponse');
}
 
public function testIndex_GivenUserExistsWithDevices_ReturnsJsonResponse(): void
{
$numberOfDevices = self::$faker->numberBetween(1, 10);
$devices = $this->createDevices($numberOfDevices);
 
$this->mockUser->shouldReceive('getAttribute')->with('devices')->once()->andReturn($devices);
 
$response = $this->callDevices();
 
$this->assertDiscoverAppliancesResponse($response, $devices);
}
 
public function testIndex_GivenUserDoesNotExist_Returns401(): void
{
$response = $this->getJson('/api/devices', [
'HTTP_Authorization' => 'Bearer ' . self::$faker->uuid(),
'HTTP_Message_Id' => $this->messageId
]);
 
$response->assertStatus(401);
$response->assertExactJson(['error' => 'User not authenticated']);
}
 
public function testInfo_GivenUserExistsWithDeviceWithRandomScope_Returns400(): void
{
$deviceId = self::$faker->randomDigit();
$action = self::$faker->word();
$this->mockUser->shouldReceive('ownsDevice')->with($deviceId)->never();
 
Passport::actingAs($this->mockUser, [self::$faker->word()]);
 
$response = $this->callInfo($deviceId, $action);
 
$response->assertStatus(400);
$response->assertExactJson(['error' => 'Missing scope']);
}
 
public function testInfo_GivenRandomUserThatExistsAndDeviceTheyDoNotOwn_Returns401(): void
{
$deviceUserDoesNotOwn = $this->createDevices()[0];
$publicDeviceId = self::$faker->uuid();
$action = self::$faker->word();
$this->mockUserOwnsDevice($deviceUserDoesNotOwn->id, false);
 
Passport::actingAs($this->mockUser, ['info']);
 
Line exceeds 120 characters; contains 136 characters
$this->mockDeviceRepository->shouldReceive('getForPublicId')->with(Mockery::on(function (Uuid $argument) use ($publicDeviceId) {
return $argument instanceof Uuid && $argument == Uuid::import($publicDeviceId);
}))->once()->andReturn($deviceUserDoesNotOwn);
 
$response = $this->callInfo($publicDeviceId, $action);
 
$response->assertStatus(401);
$response->assertExactJson(['error' => 'Unauthorized']);
}
 
public function testInfo_GivenUserExistsWithDevice_ReturnsJsonResponse(): void
{
$device = $this->createDevices()[0];
$action = self::$faker->word();
$this->mockUserOwnsDevice($device->id, true);
 
Passport::actingAs($this->mockUser, ['info']);
 
$mockDeviceActionInfoBroker = Mockery::mock(IDeviceActionInfoBroker::class);
$mockDeviceActionInfoBroker
->shouldReceive('infoNeededToPerformDeviceAction')
->once()
->withArgs([$device, $action])
->andReturn(response()->json(self::$faker->word()));
$this->app->instance(IDeviceActionInfoBroker::class, $mockDeviceActionInfoBroker);
 
Line exceeds 120 characters; contains 128 characters
$this->mockDeviceRepository->shouldReceive('getForPublicId')->with(Mockery::on(function (Uuid $argument) use ($device) {
return $argument instanceof Uuid && $argument == Uuid::import($device->public_id);
}))->times(2)->andReturn($device);
 
$response = $this->callInfo($device->public_id, $action);
 
$response->assertSuccessful();
}
 
public function testControl_GivenUserExistsWithDevice_Returns200JsonResponse(): void
{
$device = $this->createDevice();
 
$this->mockMessagePublisher(1);
 
$action = $this->randomDeviceAction();
 
$response = $this->callControl($action, $device->public_id);
 
$response->assertSuccessful();
$this->assertControlConfirmation($response, ucfirst($action . 'Confirmation'));
}
 
public function testControl_GivenUserExistsWithDevice_CallsPublishUnsuccessfully_Returns500(): void
{
$device = $this->createDevice();
 
$this->mockMessagePublisher(1, false);
 
$response = $this->callControl($this->randomDeviceAction(), $device->public_id);
 
$response->assertStatus(500);
$response->assertExactJson(['error' => 'Message not published']);
}
 
public function testControl_GivenUnknownDeviceActionForUserThatExistsWithDevice_Returns400(): void
{
$device = $this->createDevice();
 
$this->mockMessagePublisher(1);
 
$unknownAction = self::$faker->word();
 
$response = $this->callControl($unknownAction, $device->public_id);
 
$response->assertStatus(400);
$response->assertExactJson(['error' => 'Bad request']);
}
 
public function testControl_GivenUserExistsWithNoDevices_Returns401(): void
{
$device = $this->createDevice(false);
 
$response = $this->callControl($this->randomDeviceAction(), $device->public_id);
 
$response->assertStatus(401);
$response->assertExactJson(['error' => 'Unauthorized']);
}
 
private function randomDeviceAction(): string
{
$deviceActionsReflectedClass = new ReflectionClass(DeviceActions::class);
$deviceActions = $deviceActionsReflectedClass->getConstants();
 
return $deviceActions[array_rand($deviceActions)];
}
 
private function callDevices(): TestResponse
{
Passport::actingAs($this->mockUser, ['control']);
 
$response = $this->getJson('/api/devices', [
'HTTP_Authorization' => 'Bearer ' . self::$faker->uuid(),
'HTTP_Message_Id' => $this->messageId
]);
 
return $response;
}
 
private function callInfo(string $publicDeviceId, string $action): TestResponse
{
$response = $this->postJson('/api/devices/info', [
'action' => $action,
'publicDeviceId' => $publicDeviceId
]);
 
return $response;
}
 
private function callControl(string $action, string $publicDeviceId): TestResponse
{
$urlValidAction = strtolower($action);
 
Passport::actingAs($this->mockUser, ['control']);
 
$response = $this->postJson('/api/devices/control/' . $urlValidAction, ['publicDeviceId' => $publicDeviceId], [
'HTTP_Authorization' => 'Bearer ' . self::$faker->uuid(),
'HTTP_Message_Id' => $this->messageId
]);
 
return $response;
}
 
The method createDevice has a boolean flag argument $isOwnedByUser, which is a certain sign of a Single Responsibility Principle violation.
private function createDevice(bool $isOwnedByUser = true): Device
{
$device = $this->createDevices()[0];
$this->mockUserOwnsDevice($device->id, $isOwnedByUser);
Line exceeds 120 characters; contains 128 characters
$this->mockDeviceRepository->shouldReceive('getForPublicId')->with(Mockery::on(function (Uuid $argument) use ($device) {
return $argument instanceof Uuid && $argument == Uuid::import($device->public_id);
}))->once()->andReturn($device);
 
return $device;
}
 
private function assertDiscoverAppliancesResponseWithoutDevice(TestResponse $response, string $name): void
{
$response->assertExactJson([
'header' => [
'messageId' => $this->messageId,
'name' => $name,
'namespace' => 'Alexa.ConnectedHome.Discovery',
'payloadVersion' => '2'
],
'payload' => [
'discoveredAppliances' => []
]
]);
}
 
The method assertDiscoverAppliancesResponse() has 30 lines of code. Current threshold is set to 25. Avoid really long methods.
private function assertDiscoverAppliancesResponse(TestResponse $response, Collection $devices): void
{
$appliances = [];
 
for ($i = 0; $i < $devices->count(); $i++) {
$appliances[] = [
'actions' => [DeviceActions::TURN_ON, DeviceActions::TURN_OFF],
'additionalApplianceDetails' => [],
'applianceId' => $devices[$i]->id,
'friendlyName' => $devices[$i]->name,
'friendlyDescription' => $devices[$i]->description,
'isReachable' => true,
'manufacturerName' => 'N/A',
'modelName' => 'N/A',
'version' => 'N/A'
];
}
 
$response->assertExactJson([
'header' => [
'messageId' => $this->messageId,
'name' => 'DiscoverAppliancesResponse',
'namespace' => 'Alexa.ConnectedHome.Discovery',
'payloadVersion' => '2'
],
'payload' => [
'discoveredAppliances' => $appliances
]
]);
}
 
private function assertControlConfirmation(TestResponse $response, string $name): void
{
$response->assertExactJson([
'header' => [
'messageId' => $this->messageId,
'name' => $name,
'namespace' => 'Alexa.ConnectedHome.Control',
'payloadVersion' => '2'
],
'payload' => []
]);
}
 
private function createDevices(int $numberOfDevices = 1): Collection
{
return factory(Device::class, $numberOfDevices)->make([
'id' => self::$faker->randomNumber()
]);
}
 
private function createMockUser(): User
{
$publicUserId = self::$faker->uuid();
 
$mockUser = $mockUser = Mockery::mock(User::class)->makePartial();
$mockUser->shouldReceive('getAttribute')->with('public_id')->andReturn($publicUserId);
 
return $mockUser;
}
 
private function mockUserOwnsDevice(int $deviceId, bool $userOwnsDevice): void
{
$this->mockUser->shouldReceive('ownsDevice')->with($deviceId)->once()->andReturn($userOwnsDevice);
}
}