Refactor logoService to the laravel manager+driver pattern

This commit is contained in:
Bubka 2025-06-04 13:58:17 +02:00
parent 762833d168
commit bff3bd7182
14 changed files with 260 additions and 181 deletions

View File

@ -4,10 +4,10 @@ namespace App\Api\v1\Controllers;
use App\Api\v1\Requests\IconFetchRequest; use App\Api\v1\Requests\IconFetchRequest;
use App\Facades\IconStore; use App\Facades\IconStore;
use App\Facades\LogoLib;
use App\Helpers\Helpers; use App\Helpers\Helpers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\TwoFAccount; use App\Models\TwoFAccount;
use App\Services\LogoService;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
@ -48,12 +48,12 @@ class IconController extends Controller
* *
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function fetch(IconFetchRequest $request, LogoService $logoService) public function fetch(IconFetchRequest $request)
{ {
$validated = $request->validated(); $validated = $request->validated();
$icon = $logoService->getIcon($validated['service']); $icon = LogoLib::driver('tfa')->getIcon($validated['service']);
return $icon return $icon
? response()->json(['filename' => $icon], 201) ? response()->json(['filename' => $icon], 201)
: response()->json(null, 204); : response()->json(null, 204);

16
app/Facades/LogoLib.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
/**
*
*/
class LogoLib extends Facade
{
protected static function getFacadeAccessor()
{
return 'logolib';
}
}

View File

@ -5,7 +5,7 @@ namespace App\Providers;
use App\Factories\MigratorFactoryInterface; use App\Factories\MigratorFactoryInterface;
use App\Services\IconService; use App\Services\IconService;
use App\Services\IconStoreService; use App\Services\IconStoreService;
use App\Services\LogoService; use App\Services\LogoLib\LogoLibManager;
use App\Services\ReleaseRadarService; use App\Services\ReleaseRadarService;
use App\Services\SettingService; use App\Services\SettingService;
use App\Services\TwoFAccountService; use App\Services\TwoFAccountService;
@ -35,10 +35,6 @@ class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvi
return new IconStoreService($app->make(Sanitizer::class)); return new IconStoreService($app->make(Sanitizer::class));
}); });
$this->app->singleton(LogoService::class, function ($app) {
return new LogoService;
});
$this->app->singleton(IconService::class, function ($app) { $this->app->singleton(IconService::class, function ($app) {
return new IconService; return new IconService;
}); });
@ -47,6 +43,10 @@ class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvi
return new ReleaseRadarService; return new ReleaseRadarService;
}); });
$this->app->singleton('logolib', function ($app) {
return new LogoLibManager($app);
});
$this->app->bind(QrReader::class, function ($app, array $parameters) { $this->app->bind(QrReader::class, function ($app, array $parameters) {
return new QrReader($parameters['imgSource'], $parameters['sourceType']); return new QrReader($parameters['imgSource'], $parameters['sourceType']);
}); });
@ -74,7 +74,7 @@ class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvi
return [ return [
IconService::class, IconService::class,
IconStoreService::class, IconStoreService::class,
LogoService::class, LogoLibManager::class,
QrReader::class, QrReader::class,
ReleaseRadarService::class, ReleaseRadarService::class,
]; ];

View File

@ -3,8 +3,8 @@
namespace App\Services; namespace App\Services;
use App\Facades\IconStore; use App\Facades\IconStore;
use App\Facades\LogoLib;
use App\Helpers\Helpers; use App\Helpers\Helpers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -18,7 +18,7 @@ class IconService
*/ */
public function buildFromOfficialLogo(?string $service) : ?string public function buildFromOfficialLogo(?string $service) : ?string
{ {
return App::make(LogoService::class)->getIcon($service); return LogoLib::driver('tfa')->getIcon($service);
} }
/** /**

View File

@ -0,0 +1,56 @@
<?php
namespace App\Services\LogoLib;
use App\Facades\IconStore;
use App\Services\LogoLib\LogoLibInterface;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
abstract class AbstractLogoLib implements LogoLibInterface
{
/**
* Url to use in http request to get a specific logo from the logo lib
*/
abstract protected function logoUrl(string $logoFilename) : string;
/**
* Prepare service name to match logo libs naming convention
*/
abstract protected function sanitizeServiceName(string $service) : string;
/**
* Fetch and cache a logo from the logo library
*
* @param string $logoFilename Logo filename to fetch
*/
protected function fetchLogo(string $logoFilename) : void
{
try {
$response = Http::withOptions([
'proxy' => config('2fauth.config.outgoingProxy'),
])->retry(3, 100)->get($this->logoUrl($logoFilename));
if ($response->successful()) {
Storage::disk('logos')->put($logoFilename, $response->body())
? Log::info(sprintf('Logo "%s" saved to logos dir.', $logoFilename))
: Log::notice(sprintf('Cannot save logo "%s" to logos dir', $logoFilename));
}
} catch (\Exception $exception) {
Log::error(sprintf('Fetching of logo "%s" failed.', $logoFilename));
}
}
/**
* Copy a logo file to the icons store with a new name
*/
protected function copyToIconStore(string $logoFilename, string $iconFilename) : bool
{
if ($content = Storage::disk('logos')->get($logoFilename)) {
return IconStore::store($iconFilename, $content);
}
return false;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Services\LogoLib;
interface LogoLibInterface
{
public function getIcon(?string $serviceName): string|null;
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Services\LogoLib;
use App\Services\LogoLib\TfaLogoLib;
use Illuminate\Support\Manager;
class LogoLibManager extends Manager
{
public function getDefaultDriver()
{
return 'tfa';
}
public function createTfaDriver() : TfaLogoLib
{
return new TfaLogoLib();
}
// public function createSelfhDriver()
// {
// return new SelfhLogoLib();
// }
}

View File

@ -1,14 +1,15 @@
<?php <?php
namespace App\Services; namespace App\Services\LogoLib;
use App\Facades\IconStore; use App\Services\LogoLib\AbstractLogoLib;
use App\Services\LogoLib\LogoLibInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class LogoService class TfaLogoLib extends AbstractLogoLib implements LogoLibInterface
{ {
/** /**
* @var \Illuminate\Support\Collection<string, string> * @var \Illuminate\Support\Collection<string, string>
@ -28,8 +29,11 @@ class LogoService
/** /**
* @var string * @var string
*/ */
const TFA_IMG_URL = 'https://raw.githubusercontent.com/2factorauth/twofactorauth/master/img/'; const IMG_URL = 'https://raw.githubusercontent.com/2factorauth/twofactorauth/master/img/';
/**
*
*/
public function __construct() public function __construct()
{ {
$this->setTfaCollection(); $this->setTfaCollection();
@ -41,7 +45,7 @@ class LogoService
* @param string|null $serviceName Name of the service to fetch a logo for * @param string|null $serviceName Name of the service to fetch a logo for
* @return string|null The icon filename or null if no logo has been found * @return string|null The icon filename or null if no logo has been found
*/ */
public function getIcon(?string $serviceName) public function getIcon(?string $serviceName) : string|null
{ {
$logoFilename = $this->getLogo(strval($serviceName)); $logoFilename = $this->getLogo(strval($serviceName));
@ -63,7 +67,7 @@ class LogoService
*/ */
protected function getLogo(string $serviceName) protected function getLogo(string $serviceName)
{ {
$domain = $this->tfas->get($this->cleanDomain(strval($serviceName))); $domain = $this->tfas->get($this->sanitizeServiceName(strval($serviceName)));
$logoFilename = $domain . '.svg'; $logoFilename = $domain . '.svg';
if ($domain && ! Storage::disk('logos')->exists($logoFilename)) { if ($domain && ! Storage::disk('logos')->exists($logoFilename)) {
@ -118,50 +122,18 @@ class LogoService
} }
/** /**
* Fetch and cache a logo from 2fa.Directory repository * Url to use in http request to get a specific logo from the logo lib
*
* @param string $logoFile Logo filename to fetch
*/ */
protected function fetchLogo(string $logoFile) : void protected function logoUrl(string $logoFilename) : string
{ {
try { return self::IMG_URL . $logoFilename[0] . '/' . $logoFilename;
$response = Http::withOptions([
'proxy' => config('2fauth.config.outgoingProxy'),
])->retry(3, 100)->get(self::TFA_IMG_URL . $logoFile[0] . '/' . $logoFile);
if ($response->successful()) {
Storage::disk('logos')->put($logoFile, $response->body())
? Log::info(sprintf('Logo "%s" saved to logos dir.', $logoFile))
: Log::notice(sprintf('Cannot save logo "%s" to logos dir', $logoFile));
}
} catch (\Exception $exception) {
Log::error(sprintf('Fetching of logo "%s" failed.', $logoFile));
}
} }
/** /**
* Prepare and make some replacement to optimize logo fetching * Prepare service name to match logo libs naming convention
*
* @return string Optimized domain name
*/ */
protected function cleanDomain(string $domain) : string protected function sanitizeServiceName(string $service) : string
{ {
return strtolower(str_replace(['+'], ['plus'], $domain)); return strtolower(str_replace(['+'], ['plus'], $service));
}
/**
* Copy a logo file to the icons store with a new name
*
* @param string $logoFilename
* @param string $iconFilename
* @return bool Whether the copy succeed or not
*/
protected function copyToIconStore($logoFilename, $iconFilename) : bool
{
if ($content = Storage::disk('logos')->get($logoFilename)) {
return IconStore::store($iconFilename, $content);
}
return false;
} }
} }

View File

@ -6,7 +6,7 @@ use App\Api\v1\Controllers\IconController;
use App\Facades\IconStore; use App\Facades\IconStore;
use App\Models\TwoFAccount; use App\Models\TwoFAccount;
use App\Models\User; use App\Models\User;
use App\Services\LogoService; use App\Services\LogoLib\TfaLogoLib;
use Illuminate\Http\Testing\FileFactory; use Illuminate\Http\Testing\FileFactory;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -38,8 +38,8 @@ class IconControllerTest extends FeatureTestCase
Http::preventStrayRequests(); Http::preventStrayRequests();
Http::fake([ Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200), TfaLogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200), TfalogoLib::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]); ]);
Http::fake([ Http::fake([
OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200), OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200),

View File

@ -16,7 +16,7 @@ use App\Models\User;
use App\Policies\TwoFAccountPolicy; use App\Policies\TwoFAccountPolicy;
use App\Providers\MigrationServiceProvider; use App\Providers\MigrationServiceProvider;
use App\Providers\TwoFAuthServiceProvider; use App\Providers\TwoFAuthServiceProvider;
use App\Services\LogoService; use App\Services\LogoLib\TfaLogoLib;
use Illuminate\Http\Testing\FileFactory; use Illuminate\Http\Testing\FileFactory;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -242,8 +242,8 @@ class TwoFAccountControllerTest extends FeatureTestCase
Http::preventStrayRequests(); Http::preventStrayRequests();
Http::fake([ Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200), TfaLogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200), TfaLogoLib::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200), OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200),
OtpTestData::EXTERNAL_INFECTED_IMAGE_URL_DECODED => Http::response((new FileFactory)->createWithContent('infected.svg', OtpTestData::ICON_SVG_DATA_INFECTED)->tempFile, 200), OtpTestData::EXTERNAL_INFECTED_IMAGE_URL_DECODED => Http::response((new FileFactory)->createWithContent('infected.svg', OtpTestData::ICON_SVG_DATA_INFECTED)->tempFile, 200),
'example.com/*' => Http::response(null, 400), 'example.com/*' => Http::response(null, 400),

View File

@ -5,7 +5,7 @@ namespace Tests\Feature\Models;
use App\Facades\Icons; use App\Facades\Icons;
use App\Models\TwoFAccount; use App\Models\TwoFAccount;
use App\Models\User; use App\Models\User;
use App\Services\LogoService; use App\Services\LogoLib\TfaLogoLib;
use Illuminate\Http\Testing\FileFactory; use Illuminate\Http\Testing\FileFactory;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -269,8 +269,8 @@ class TwoFAccountModelTest extends FeatureTestCase
$this->user['preferences->getOfficialIcons'] = false; $this->user['preferences->getOfficialIcons'] = false;
$this->user->save(); $this->user->save();
$this->mock(LogoService::class, function (MockInterface $logoService) { $this->mock(TfaLogoLib::class, function (MockInterface $logoLib) {
$logoService->shouldNotReceive('getIcon'); $logoLib->shouldNotReceive('getIcon');
}); });
$twofaccount = new TwoFAccount; $twofaccount = new TwoFAccount;

View File

@ -2,8 +2,9 @@
namespace Tests\Feature\Services; namespace Tests\Feature\Services;
use App\Facades\LogoLib;
use App\Services\IconService; use App\Services\IconService;
use App\Services\LogoService; use App\Services\LogoLib\TfaLogoLib;
use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\Testing\FileFactory; use Illuminate\Http\Testing\FileFactory;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -38,8 +39,8 @@ class IconServiceTest extends FeatureTestCase
Http::preventStrayRequests(); Http::preventStrayRequests();
Http::fake([ Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200), TfaLogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200), TfaLogoLib::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]); ]);
Http::fake([ Http::fake([
OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200), OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200),
@ -47,14 +48,16 @@ class IconServiceTest extends FeatureTestCase
} }
#[Test] #[Test]
public function test_buildFromOfficialLogo_calls_logoservice_to_get_the_icon() public function test_buildFromOfficialLogo_calls_logoLib_to_get_the_icon()
{ {
$logoServiceSpy = $this->spy(LogoService::class); // LogoLib::spy();
LogoLib::shouldReceive('driver->getIcon')
->once()
->with('fakeService')
->andReturn('value');
$this->iconService = $this->app->make(IconService::class); $this->iconService = $this->app->make(IconService::class);
$this->iconService->buildFromOfficialLogo('fakeService'); $this->iconService->buildFromOfficialLogo('fakeService');
$logoServiceSpy->shouldHaveReceived('getIcon')->once()->with('fakeService');
} }
#[Test] #[Test]

View File

@ -0,0 +1,109 @@
<?php
namespace Tests\Feature\Services;
use App\Services\LogoLib\TfaLogoLib;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\Data\HttpRequestTestData;
use Tests\FeatureTestCase;
/**
* TfalogoLibTest test class
*/
#[CoversClass(TfalogoLib::class)]
class TfaLogoLibTest extends FeatureTestCase
{
use WithoutMiddleware;
protected TfaLogoLib $tfaLogoLib;
public function setUp() : void
{
parent::setUp();
Storage::fake('icons');
Storage::fake('logos');
Storage::fake('imagesLink');
}
#[Test]
public function test_getIcon_returns_stored_icon_file_when_logo_exists()
{
Http::preventStrayRequests();
Http::fake([
TfalogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
TfalogoLib::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]);
$this->tfaLogoLib = $this->app->make(TfalogoLib::class);
$icon = $this->tfaLogoLib->getIcon('service');
$this->assertNotNull($icon);
Storage::disk('icons')->assertExists($icon);
}
#[Test]
public function test_getIcon_returns_null_when_github_request_fails()
{
Http::preventStrayRequests();
Http::fake([
TfalogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
TfalogoLib::TFA_URL => Http::response('not found', 404),
]);
$this->tfaLogoLib = $this->app->make(TfalogoLib::class);
$icon = $this->tfaLogoLib->getIcon('service');
$this->assertEquals(null, $icon);
}
#[Test]
public function test_getIcon_returns_null_when_logo_fetching_fails()
{
Http::preventStrayRequests();
Http::fake([
TfalogoLib::IMG_URL . '*' => Http::response('not found', 404),
TfalogoLib::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]);
$this->tfaLogoLib = $this->app->make(TfalogoLib::class);
$icon = $this->tfaLogoLib->getIcon('service');
$this->assertEquals(null, $icon);
}
#[Test]
public function test_getIcon_returns_null_when_no_logo_exists()
{
Http::preventStrayRequests();
Http::fake([
TfalogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
TfalogoLib::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]);
$this->tfaLogoLib = $this->app->make(TfalogoLib::class);
$icon = $this->tfaLogoLib->getIcon('no_logo_should_exists_with_this_name');
$this->assertEquals(null, $icon);
}
#[Test]
public function test_TfalogoLib_loads_empty_collection_when_tfajson_fetching_fails()
{
Http::preventStrayRequests();
Http::fake([
TfalogoLib::IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
TfalogoLib::TFA_URL => Http::response('not found', 404),
]);
$this->tfaLogoLib = $this->app->make(TfalogoLib::class);
$icon = $this->tfaLogoLib->getIcon('service');
$this->assertNull($icon);
Storage::disk('logos')->assertMissing(TfalogoLib::TFA_JSON);
}
}

View File

@ -1,109 +0,0 @@
<?php
namespace Tests\Feature\Services;
use App\Services\LogoService;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\Data\HttpRequestTestData;
use Tests\FeatureTestCase;
/**
* LogoServiceTest test class
*/
#[CoversClass(LogoService::class)]
class LogoServiceTest extends FeatureTestCase
{
use WithoutMiddleware;
protected LogoService $logoService;
public function setUp() : void
{
parent::setUp();
Storage::fake('icons');
Storage::fake('logos');
Storage::fake('imagesLink');
}
#[Test]
public function test_getIcon_returns_stored_icon_file_when_logo_exists()
{
Http::preventStrayRequests();
Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]);
$this->logoService = $this->app->make(LogoService::class);
$icon = $this->logoService->getIcon('service');
$this->assertNotNull($icon);
Storage::disk('icons')->assertExists($icon);
}
#[Test]
public function test_getIcon_returns_null_when_github_request_fails()
{
Http::preventStrayRequests();
Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response('not found', 404),
]);
$this->logoService = $this->app->make(LogoService::class);
$icon = $this->logoService->getIcon('service');
$this->assertEquals(null, $icon);
}
#[Test]
public function test_getIcon_returns_null_when_logo_fetching_fails()
{
Http::preventStrayRequests();
Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response('not found', 404),
LogoService::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]);
$this->logoService = $this->app->make(LogoService::class);
$icon = $this->logoService->getIcon('service');
$this->assertEquals(null, $icon);
}
#[Test]
public function test_getIcon_returns_null_when_no_logo_exists()
{
Http::preventStrayRequests();
Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
]);
$this->logoService = $this->app->make(LogoService::class);
$icon = $this->logoService->getIcon('no_logo_should_exists_with_this_name');
$this->assertEquals(null, $icon);
}
#[Test]
public function test_logoService_loads_empty_collection_when_tfajson_fetching_fails()
{
Http::preventStrayRequests();
Http::fake([
LogoService::TFA_IMG_URL . '*' => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
LogoService::TFA_URL => Http::response('not found', 404),
]);
$this->logoService = $this->app->make(LogoService::class);
$icon = $this->logoService->getIcon('service');
$this->assertNull($icon);
Storage::disk('logos')->assertMissing(LogoService::TFA_JSON);
}
}