Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion Model/Rules.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Sansec\Shield\Model;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Flag;
use Magento\Framework\Flag\FlagResource;
use Magento\Framework\FlagFactory;
Expand All @@ -15,6 +16,9 @@ class Rules
{
private const PROTOCOL_VERSION = '1';
private const FLAG_CODE = 'sansec_shield_rules';
private const CACHE_KEY = 'sansec_shield_rules';
private const CACHE_TAG = 'SANSEC_SHIELD';
private const CACHE_LIFETIME = 300;

/** @var Config */
private $config;
Expand All @@ -37,14 +41,18 @@ class Rules
/** @var DateTime */
private $dateTime;

/** @var CacheInterface */
private $cache;

public function __construct(
Config $config,
FlagFactory $flagFactory,
FlagResource $flagResource,
SerializerInterface $serializer,
CurlFactory $curlFactory,
ModuleDirReader $moduleDirReader,
DateTime $dateTime
DateTime $dateTime,
CacheInterface $cache
) {
$this->config = $config;
$this->flagFactory = $flagFactory;
Expand All @@ -53,9 +61,52 @@ public function __construct(
$this->curlFactory = $curlFactory;
$this->moduleDirReader = $moduleDirReader;
$this->dateTime = $dateTime;
$this->cache = $cache;
}

public function loadRules(): array
{
$rules = $this->loadRulesFromCache();
if ($rules !== null) {
return $rules;
}

$rules = $this->loadRulesFromFlag();
if ($rules !== []) {
try {
$this->saveRulesToCache($rules);
} catch (\Throwable $exception) {
return $rules;
}
}
return $rules;
}

private function loadRulesFromCache(): ?array
{
try {
$rulesData = $this->cache->load(self::CACHE_KEY);
if (!is_string($rulesData) || $rulesData === '') {
return null;
}
$rules = $this->serializer->unserialize($rulesData);
} catch (\Throwable $exception) {
return null;
}
return is_array($rules) && $rules !== [] ? $rules : null;
}

private function saveRulesToCache(array $rules): void
{
$this->cache->save(
$this->serializer->serialize($rules),
self::CACHE_KEY,
[self::CACHE_TAG],
self::CACHE_LIFETIME
);
}

private function loadRulesFromFlag(): array
{
try {
$rulesData = $this->loadFlag()->getFlagData();
Expand Down Expand Up @@ -166,6 +217,7 @@ private function saveFlag(array $rules): void
$flag->setFlagData($rules);
$flag->setData('last_update', $this->dateTime->gmtDate());
$this->flagResource->save($flag);
$this->saveRulesToCache($rules);
}

private function deleteFlag(): void
Expand All @@ -174,5 +226,6 @@ private function deleteFlag(): void
if ($flag->getId()) {
$this->flagResource->delete($flag);
}
$this->cache->remove(self::CACHE_KEY);
}
}
157 changes: 157 additions & 0 deletions Test/Model/RulesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace Sansec\Shield\Test\Model;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Flag;
use Magento\Framework\Flag\FlagResource;
use Magento\Framework\FlagFactory;
use Magento\Framework\HTTP\Client\CurlFactory;
use Magento\Framework\Module\Dir\Reader as ModuleDirReader;
use Magento\Framework\Stdlib\DateTime\DateTime;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sansec\Shield\Model\Config;
use Sansec\Shield\Model\Rules;
use Sansec\Shield\Model\Serializer;

class RulesTest extends TestCase
{
private const CACHE_KEY = 'sansec_shield_rules';
private const CACHE_LIFETIME = 300;

/** @var array */
private $rules = [['id' => 1, 'conditions' => []]];

/** @var FlagFactory|MockObject */
private $flagFactory;

/** @var FlagResource|MockObject */
private $flagResource;

/** @var CacheInterface|MockObject */
private $cache;

/** @var Flag|MockObject */
private $flag;

/** @var Rules */
private $rulesModel;

public function setUp(): void
{
parent::setUp();

$this->flag = $this->createMock(Flag::class);
$this->flagFactory = $this->createMock(FlagFactory::class);
$this->flagFactory->method('create')->willReturn($this->flag);
$this->flagResource = $this->createMock(FlagResource::class);
$this->cache = $this->createMock(CacheInterface::class);

$this->rulesModel = new Rules(
$this->createMock(Config::class),
$this->flagFactory,
$this->flagResource,
new Serializer(),
$this->createCurlFactoryMock(),
$this->createMock(ModuleDirReader::class),
$this->createMock(DateTime::class),
$this->cache
);
}

public function testCacheHitDoesNotTouchFlag()
{
$this->cache->method('load')->with(self::CACHE_KEY)->willReturn(json_encode($this->rules));
$this->cache->expects($this->never())->method('save');
$this->flagResource->expects($this->never())->method('load');

$this->assertEquals($this->rules, $this->rulesModel->loadRules());
}

public function testCacheMissLoadsFlagAndPopulatesCache()
{
$this->cache->method('load')->willReturn(false);
$this->flagResource->expects($this->once())->method('load');
$this->flag->method('getFlagData')->willReturn($this->rules);
$this->cache->expects($this->once())
->method('save')
->with(json_encode($this->rules), self::CACHE_KEY, ['SANSEC_SHIELD'], self::CACHE_LIFETIME);

$this->assertEquals($this->rules, $this->rulesModel->loadRules());
}

public function testCorruptCacheEntryFallsBackToFlag()
{
$this->cache->method('load')->willReturn('{invalid json');
$this->flag->method('getFlagData')->willReturn($this->rules);
$this->cache->expects($this->once())->method('save');

$this->assertEquals($this->rules, $this->rulesModel->loadRules());
}

public function testFailingCacheWriteStillReturnsFlagRules()
{
$this->cache->method('load')->willReturn(false);
$this->flag->method('getFlagData')->willReturn($this->rules);
$this->cache->method('save')->willThrowException(new \RuntimeException('backend down'));

$this->assertEquals($this->rules, $this->rulesModel->loadRules());
}

public function testMissingFlagIsNotCached()
{
$this->cache->method('load')->willReturn(false);
$this->flag->method('getFlagData')->willReturn(null);
$this->cache->expects($this->never())->method('save');

$this->assertEquals([], $this->rulesModel->loadRules());
}

public function testSaveFlagWritesCache()
{
$this->flagResource->expects($this->once())->method('save')->with($this->flag);
$this->cache->expects($this->once())
->method('save')
->with(json_encode($this->rules), self::CACHE_KEY, ['SANSEC_SHIELD'], self::CACHE_LIFETIME);

$this->invoke('saveFlag', [$this->rules]);
}

public function testDeleteFlagRemovesCache()
{
$this->flag->method('getId')->willReturn(1);
$this->flagResource->expects($this->once())->method('delete')->with($this->flag);
$this->cache->expects($this->once())->method('remove')->with(self::CACHE_KEY);

$this->invoke('deleteFlag', []);
}

public function testLegacyFlagFormatDeletesFlagAndCache()
{
$this->cache->method('load')->willReturn(false);
$this->flag->method('getFlagData')->willReturn(json_encode($this->rules));
$this->flag->method('getId')->willReturn(1);
$this->flagResource->expects($this->once())->method('delete');
$this->cache->expects($this->once())->method('remove')->with(self::CACHE_KEY);
$this->cache->expects($this->never())->method('save');

$this->assertEquals([], $this->rulesModel->loadRules());
}

private function createCurlFactoryMock(): CurlFactory
{
return $this->getMockBuilder(CurlFactory::class)
->disableOriginalConstructor()
->disableAutoload()
->setMethods(['create'])
->getMock();
}

private function invoke(string $method, array $arguments)
{
$reflection = new \ReflectionMethod(Rules::class, $method);
$reflection->setAccessible(true);
return $reflection->invokeArgs($this->rulesModel, $arguments);
}
}