Commit 42f65df0 authored by Jacob Priddy's avatar Jacob Priddy 👌

Merge branch '69-add-door-controller-update-route' into 'master'

Resolve "Add door controller update route"

Closes #69

See merge request !59
parents 2b779212 50b21281
Pipeline #9791 canceled with stages
in 2 minutes and 14 seconds
......@@ -8,7 +8,10 @@ use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Source\Authorization\Authorizer;
use Source\UseCases\Door\Access\AccessUseCase;
use Source\UseCases\Door\UpdateBinary\FilePresenter;
use Source\UseCases\Door\StatusResponse\JsonPresenter;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase;
use Source\UseCases\Door\StatusResponse\StatusResponseUseCase;
class DoorController extends ApiController
......@@ -68,4 +71,19 @@ class DoorController extends ApiController
{
return $this->respondStatus();
}
/**
* @param \Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase $updateBinaryUseCase
* @param \App\Guards\DoorGuard $doorGuard
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function getUpdateBinary(UpdateBinaryUseCase $updateBinaryUseCase, DoorGuard $doorGuard): BinaryFileResponse
{
$presenter = new FilePresenter();
$updateBinaryUseCase->getUpdateFile($doorGuard->id(), $presenter);
return response()->download($presenter->getUpdateFilePath());
}
}
......@@ -20,6 +20,7 @@ use Source\Gateways\DoorGroup\DoorGroupRepositoryServiceProvider;
use Source\Gateways\GroupUser\GroupUserRepositoryServiceProvider;
use Source\Gateways\Overrides\OverridesRepositoryServiceProvider;
use Source\Gateways\Schedules\SchedulesRepositoryServiceProvider;
use Source\Gateways\Filesystem\FilesystemRepositoryServiceProvider;
use Source\UseCases\Groups\GetGroup\GetGroupUseCaseServiceProvider;
use Source\UseCases\Tokens\GetToken\GetTokenUseCaseServiceProvider;
use Source\UseCases\Doors\CreateDoor\CreateDoorUseCaseServiceProvider;
......@@ -32,6 +33,7 @@ use Source\Gateways\DoorSchedule\DoorScheduleRepositoryServiceProvider;
use Source\UseCases\Doors\GetAllDoors\GetAllDoorsUseCaseServiceProvider;
use Source\UseCases\Users\GetAllUsers\GetAllUsersUseCaseServiceProvider;
use Source\Gateways\RecurrenceSet\RecurrenceSetRepositoryServiceProvider;
use Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCaseServiceProvider;
use Source\UseCases\Groups\CreateGroup\CreateGroupUseCaseServiceProvider;
use Source\UseCases\Groups\DeleteGroup\DeleteGroupUseCaseServiceProvider;
use Source\UseCases\Groups\UpdateGroup\UpdateGroupUseCaseServiceProvider;
......@@ -93,6 +95,7 @@ class AppServiceProvider extends ServiceProvider
DoorGroupRepositoryServiceProvider::class,
GroupUserRepositoryServiceProvider::class,
OverridesRepositoryServiceProvider::class,
FilesystemRepositoryServiceProvider::class,
DoorScheduleRepositoryServiceProvider::class,
RecurrenceSetRepositoryServiceProvider::class,
];
......@@ -132,6 +135,7 @@ class AppServiceProvider extends ServiceProvider
// Door
AccessUseCaseServiceProvider::class,
UpdateBinaryUseCaseServiceProvider::class,
StatusResponseUseCaseServiceProvider::class,
DoorAuthenticateUseCaseServiceProvider::class,
......
......@@ -28,6 +28,8 @@ return [
'cloud' => env('FILESYSTEM_CLOUD', 's3'),
'controller_update_path' => 'controller/binaries/',
/*
|--------------------------------------------------------------------------
| Filesystem Disks
......
......@@ -22,3 +22,5 @@ Route::get('ping', static function () {
Route::get('access/{doorcode}', [DoorController::class, 'access']);
Route::get('status', [DoorController::class, 'status']);
Route::get('update', [DoorController::class, 'getUpdateBinary']);
<?php
namespace Source\Gateways\Filesystem;
interface FilesystemRepository
{
/**
* Returns a list of files in a directory (Not recursive).
*
* @param string $directory
* @return string[]
*/
public function getFilesForDirectory(string $directory): array;
/**
* Gets the full path to the file
*
* @param string $file
* @return string
*/
public function getFullPathForFIle(string $file): string;
}
<?php
namespace Source\Gateways\Filesystem;
use Illuminate\Support\ServiceProvider;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Support\DeferrableProvider;
/**
* Service provider must be registered in AppServiceProvider
*/
class FilesystemRepositoryServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(FilesystemRepository::class, static function (Application $app) {
if (env('APP_ENV') === 'testing') {
return new InMemoryFilesystemRepository();
}
/** @var FilesystemManager $fsManager */
$fsManager = $app->make(FilesystemManager::class);
return new OperatingSystemFilesystemRepository($fsManager->disk('local'));
});
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
}
/**
* @return array
*/
public function provides()
{
return [FilesystemRepository::class];
}
}
<?php
namespace Source\Gateways\Filesystem;
class InMemoryFilesystemRepository implements FilesystemRepository
{
/**
* @var string
*/
public string $basePath = '';
/**
* @var string[][]
*/
protected array $files = [];
/**
* @param string $directory
* @param string $file
*/
public function addFileToDirectory(string $directory, string $file): void
{
if (isset($this->files[$directory])) {
$this->files[$directory][] = $file;
} else {
$this->files[$directory] = [$file];
}
}
/**
* @inheritDoc
*/
public function getFilesForDirectory(string $directory): array
{
return $this->files[$directory] ?? [];
}
/**
* @inheritDoc
*/
public function getFullPathForFIle(string $file): string
{
return $this->basePath . $file;
}
}
<?php
namespace Source\Gateways\Filesystem;
use Illuminate\Contracts\Filesystem\Filesystem;
class OperatingSystemFilesystemRepository implements FilesystemRepository
{
/**
* @var \Illuminate\Contracts\Filesystem\Filesystem
*/
protected Filesystem $fs;
/**
* @param \Illuminate\Contracts\Filesystem\Filesystem $fs
*/
public function __construct(Filesystem $fs)
{
$this->fs = $fs;
}
/**
* @inheritDoc
*/
public function getFullPathForFIle(string $file): string
{
return $this->fs->getDriver()->getAdapter()->getPathPrefix() . $file;
}
/**
* @inheritDoc
*/
public function getFilesForDirectory(string $directory): array
{
return array_filter($this->fs->files($directory), static function (string $file) {
// Filter out .gitignore file
return strpos($file, '.gitignore') === false;
});
}
}
<?php
namespace Source\UseCases\Door\UpdateBinary;
use Source\UseCases\BasePresenter;
class FilePresenter extends BasePresenter implements Presenter
{
/**
* @var string
*/
protected string $updateFile;
/** @inheritDoc */
public function present(ResponseModel $responseModel): void
{
$this->updateFile = $responseModel->getPath();
}
/** @inheritDoc */
public function getUpdateFilePath(): string
{
return $this->updateFile;
}
}
<?php
namespace Source\UseCases\Door\UpdateBinary;
interface Presenter
{
/**
* @param ResponseModel $responseModel
* @return void
*/
public function present(ResponseModel $responseModel): void;
/**
* @return string
*/
public function getUpdateFilePath(): string;
}
<?php
namespace Source\UseCases\Door\UpdateBinary;
class ResponseModel
{
protected string $path;
public function __construct(string $path)
{
$this->path = $path;
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
}
<?php
namespace Source\UseCases\Door\UpdateBinary;
use Source\Exceptions\EntityNotFoundException;
use Source\Gateways\Filesystem\FilesystemRepository;
class UpdateBinary implements UpdateBinaryUseCase
{
/**
* @var \Source\Gateways\Filesystem\FilesystemRepository
*/
protected FilesystemRepository $fs;
protected string $updateBinaryDirectory;
public function __construct(FilesystemRepository $fs, string $updateBinaryDirectory)
{
$this->fs = $fs;
$this->updateBinaryDirectory = $updateBinaryDirectory;
}
/**
* @inheritDoc
*/
public function getUpdateFile(?string $doorId, Presenter $presenter): void
{
if (!$doorId || !$this->updateBinaryDirectory) {
// no door exists to find an update for
// Or there is no update directory specified
throw new EntityNotFoundException();
}
$binaries = $this->fs->getFilesForDirectory($this->updateBinaryDirectory);
if (!$binaries) {
throw new EntityNotFoundException();
}
rsort($binaries);
$response = new ResponseModel($this->fs->getFullPathForFIle($binaries[0]));
$presenter->present($response);
}
}
<?php
namespace Source\UseCases\Door\UpdateBinary;
interface UpdateBinaryUseCase
{
/**
* Determines the update binary to send to the specified door for an update
* Throws EntityNotFoundException if it cannot find a suitable update file
*
* @param string $doorId
* @param \Source\UseCases\Door\UpdateBinary\Presenter $presenter
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function getUpdateFile(?string $doorId, Presenter $presenter): void;
}
<?php
namespace Source\UseCases\Door\UpdateBinary;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Support\DeferrableProvider;
use Source\Gateways\Filesystem\FilesystemRepository;
/**
* Service provider must be registered in AppServiceProvider
*/
class UpdateBinaryUseCaseServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind(UpdateBinaryUseCase::class, static function (Application $app) {
return new UpdateBinary(
$app->make(FilesystemRepository::class),
config('filesystems.controller_update_path', '')
);
});
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
}
/**
* @return array
*/
public function provides()
{
return [UpdateBinaryUseCase::class];
}
}
<?php
namespace Tests\Feature\Door;
use Illuminate\Testing\TestResponse;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Contracts\Filesystem\Filesystem;
use Source\UseCases\Door\UpdateBinary\UpdateBinary;
use Tests\Feature\AuthenticatesWithApplicationTestCase;
use Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase;
use Source\Gateways\Filesystem\OperatingSystemFilesystemRepository;
class DoorUpdateTest extends AuthenticatesWithApplicationTestCase
{
protected const LATEST_BIN_NAME = 'z';
protected TestResponse $response;
protected string $path;
/**
* @var \Illuminate\Contracts\Filesystem\Filesystem
*/
protected Filesystem $disk;
public function setUp(): void
{
parent::setUp();
/** @var FilesystemManager $fs */
$fs = $this->app->make(FilesystemManager::class);
$this->disk = $fs->disk('local');
$this->path = config('filesystems.controller_update_path');
$this->app->bind(UpdateBinaryUseCase::class, function () {
return new UpdateBinary(new OperatingSystemFilesystemRepository($this->disk), $this->path);
});
}
protected function handleTest(): void
{
$this->response = $this->get('/api/door/update', [
'Authorization' => 'Bearer ' . $this->doorToken
]);
}
/**
* @param string $name
* @param string $contents
*/
protected function addFile(string $name, string $contents): void
{
$this->disk->put($this->path . $name, $contents);
}
/**
* @param string $name
*/
protected function removeFile(string $name): void
{
$this->disk->delete($this->path . $name);
}
/**
* @test
*/
public function it_denies_unauthenticated_doors(): void
{
$this->handleTest();
$this->response->assertStatus(401);
}
/**
* @test
* @throws \Source\Exceptions\EntityExistsException
*/
public function it_gets_a_file_update(): void
{
$this->addFile(self::LATEST_BIN_NAME, 'file contents');
$this->authenticateAsDoor();
$this->handleTest();
$this->removeFile(self::LATEST_BIN_NAME);
$this->response->assertStatus(200);
$this->response->assertHeader('content-length', 13);
}
}
<?php
namespace Tests\Unit\Source\UseCases\Door\UpdateBinary;
use PHPUnit\Framework\TestCase;
use Source\Exceptions\EntityNotFoundException;
use Source\UseCases\Door\UpdateBinary\UpdateBinary;
use Source\UseCases\Door\UpdateBinary\FilePresenter;
use Source\Gateways\Filesystem\InMemoryFilesystemRepository;
class UpdateBinaryTest extends TestCase
{
protected const UPDATE_BINARY_DIR = '265918';
protected const DOOR_ID = '264557';
/**
* @var \Source\Gateways\Filesystem\InMemoryFilesystemRepository
*/
protected InMemoryFilesystemRepository $fs;
/**
* @var \Source\UseCases\Door\UpdateBinary\UpdateBinary
*/
protected UpdateBinary $useCase;
protected string $returnedPath;
public function setUp(): void
{
parent::setUp();
$this->fs = new InMemoryFilesystemRepository();
$this->useCase = new UpdateBinary($this->fs, self::UPDATE_BINARY_DIR);
}
/**
* @param string|null $doorId
* @throws \Source\Exceptions\EntityNotFoundException
*/
protected function handleTest(?string $doorId): void
{
$presenter = new FilePresenter();
$this->useCase->getUpdateFile($doorId, $presenter);
$this->returnedPath = $presenter->getUpdateFilePath();
}
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_fails_when_there_is_no_door(): void
{
$this->expectException(EntityNotFoundException::class);
$this->handleTest(null);
}
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_fails_when_there_is_no_specified_update_directory(): void
{