Skip to content

Commit 1ef90b2

Browse files
committed
Add integration tests
1 parent 6ffe7e0 commit 1ef90b2

File tree

9 files changed

+360
-1
lines changed

9 files changed

+360
-1
lines changed

Plugin/SectionLoadControllerPlugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use \Magento\Framework\Message\Session as MessageSession;
99
use \Magento\Catalog\Model\Session as CatalogSession;
1010

11-
/*
11+
/**
1212
* We are writing this plugin to make sure sessions are all loaded before
1313
* Magento\Customer\Controller\Section\Load
1414
*

Plugin/SessionStoragePlugin.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Plugin;
5+
6+
use IntegerNet\SessionUnblocker\Test\Util\MethodLog;
7+
use Magento\Framework\Session\Storage;
8+
use Magento\Framework\Session\StorageInterface;
9+
10+
/**
11+
* This plugin is currently only used for testing and debugging. It can detect, when sessions are started and modified.
12+
*/
13+
class SessionStoragePlugin
14+
{
15+
/**
16+
* @var bool
17+
*/
18+
private $doLogMethods = false;
19+
20+
/**
21+
* @var MethodLog
22+
*/
23+
private $methodLog;
24+
25+
public function __construct(MethodLog $methodLog, bool $doLogMethods = false)
26+
{
27+
$this->doLogMethods = $doLogMethods;
28+
$this->methodLog = $methodLog;
29+
}
30+
31+
public function beforeSetData(Storage $subject, $key, $value = null)
32+
{
33+
if ($this->doLogMethods) {
34+
if (is_array($key)) {
35+
$this->methodLog->logSessionModify(
36+
get_parent_class($subject),
37+
'setData',
38+
$subject->getNamespace(),
39+
40+
'[...]'
41+
);
42+
} elseif ($subject->getData($key) !== $value) {
43+
$this->methodLog->logSessionModify(
44+
get_parent_class($subject),
45+
'setData',
46+
$subject->getNamespace(),
47+
$key
48+
);
49+
}
50+
}
51+
52+
return [$key, $value];
53+
}
54+
55+
public function beforeInit(StorageInterface $subject, array $data)
56+
{
57+
if ($this->doLogMethods) {
58+
MethodLog::instance()->logSessionStart(get_parent_class($subject), 'init', $subject->getNamespace());
59+
}
60+
return [$data];
61+
}
62+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Test\Integration;
5+
6+
use IntegerNet\SessionUnblocker\Plugin\SessionStoragePlugin;
7+
use IntegerNet\SessionUnblocker\Test\Util\MethodLog;
8+
use IntegerNet\SessionUnblocker\Test\Util\ProfilerSpy;
9+
use IntegerNet\SessionUnblocker\Test\Util\SectionLoadActionSpy;
10+
use IntegerNet\SessionUnblocker\Test\Util\SessionSpy;
11+
use IntegerNet\SessionUnblocker\Test\Util\SessionSpyCustomer;
12+
use IntegerNet\SessionUnblocker\Test\Util\SessionSpyMessage;
13+
use IntegerNet\SessionUnblocker\Test\Util\SessionStorageSpy;
14+
use Magento\Customer\Controller\Section\Load as LoadAction;
15+
use Magento\Framework\Session\Generic as GenericSession;
16+
use Magento\TestFramework\Helper\Bootstrap;
17+
use Magento\TestFramework\ObjectManager;
18+
use Magento\TestFramework\TestCase\AbstractController;
19+
20+
/**
21+
* @magentoAppIsolation enabled
22+
* @magentoAppArea frontend
23+
*/
24+
abstract class AbstractSessionTest extends AbstractController
25+
{
26+
/**
27+
* @var ObjectManager
28+
*/
29+
private $objectManager;
30+
31+
protected function setUp()
32+
{
33+
parent::setUp();
34+
$this->objectManager = Bootstrap::getObjectManager();
35+
$this->setUpSpies();
36+
}
37+
38+
protected function given_session_already_exists(): void
39+
{
40+
$this->dispatch('customer/account/login');
41+
MethodLog::instance()->reset();
42+
}
43+
44+
protected function when_dispatched($uri): void
45+
{
46+
$_SERVER['REQUEST_METHOD'] = 'GET'; // to make \Magento\Framework\App\Request\Http::isSafeMethod() return true
47+
$this->dispatch($uri);
48+
}
49+
50+
protected function then_sessions_have_been_started_and_closed_before_action()
51+
{
52+
$methodLog = MethodLog::instance();
53+
$this->assertTrue(
54+
$methodLog->hasSessionWriteCloseBeforeAction(),
55+
"Session should be closed before controller action. Method log: \n" .
56+
$methodLog->asString()
57+
);
58+
$this->assertFalse(
59+
$methodLog->hasSessionStartAfterAction(),
60+
"No session should be initialized during or after controller action. Method log: \n" .
61+
$methodLog->asString()
62+
);
63+
$this->assertTrue(
64+
$methodLog->hasSessionStartBeforeAction(),
65+
"Session should be initialized before controller action. Method log: \n" .
66+
$methodLog->asString()
67+
);
68+
}
69+
70+
protected function then_sessions_have_not_been_modified_after_write()
71+
{
72+
$methodLog = MethodLog::instance();
73+
$this->assertFalse(
74+
$methodLog->hasModifyAfterSessionWriteClose(),
75+
"Session should not be modified after close. Method log: \n" .
76+
$methodLog->asString()
77+
);
78+
}
79+
80+
private function setUpSpies(): void
81+
{
82+
$this->objectManager->configure(
83+
[
84+
'preferences' => [
85+
LoadAction::class => SectionLoadActionSpy::class,
86+
GenericSession::class => SessionSpy::class,
87+
],
88+
SessionStoragePlugin::class => [
89+
'arguments' => ['doLogMethods' => true]
90+
]
91+
]
92+
);
93+
}
94+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Test\Integration;
5+
6+
class CustomerSectionLoadControllerTest extends AbstractSessionTest
7+
{
8+
public function testCustomerSectionLoad()
9+
{
10+
$this->when_dispatched('customer/section/load');
11+
$this->then_sessions_have_been_started_and_closed_before_action();
12+
}
13+
14+
public function testCustomerSectionLoadNoWrites()
15+
{
16+
$this->given_session_already_exists();
17+
$this->when_dispatched('customer/section/load');
18+
$this->then_sessions_have_not_been_modified_after_write();
19+
}
20+
}

Test/Integration/ModuleTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Test\Integration;
5+
6+
use Magento\Framework\App\ObjectManager;
7+
use Magento\Framework\Module\ModuleList;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class ModuleTest extends TestCase
11+
{
12+
private const MODULE_NAME = 'IntegerNet_SessionUnblocker';
13+
14+
/**
15+
* @var ObjectManager
16+
*/
17+
private $objectManager;
18+
19+
protected function setUp()
20+
{
21+
$this->objectManager = ObjectManager::getInstance();
22+
}
23+
24+
public function testModuleIsActive()
25+
{
26+
/** @var ModuleList $moduleList */
27+
$moduleList = $this->objectManager->create(ModuleList::class);
28+
$this->assertTrue(
29+
$moduleList->has(self::MODULE_NAME),
30+
sprintf('The module %s should be enabled', self::MODULE_NAME)
31+
);
32+
}
33+
}

Test/Util/MethodLog.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Test\Util;
5+
6+
use Magento\TestFramework\Helper\Bootstrap;
7+
8+
class MethodLog
9+
{
10+
/**
11+
* @var string[]
12+
*/
13+
private $calls = [];
14+
/**
15+
* @var string[]
16+
*/
17+
private $callDetails = [];
18+
19+
private const SESSION_START = 'session_start';
20+
21+
private const SESSION_MODIFY = 'session_modify';
22+
23+
private const CONTROLLER_ACTION = 'controller_action';
24+
25+
private const SESSION_WRITE_CLOSE = 'session_write_close';
26+
27+
public static function instance(): self
28+
{
29+
// should only be used in integration test context
30+
if (class_exists(Bootstrap::class)) {
31+
return Bootstrap::getObjectManager()->get(MethodLog::class);
32+
}
33+
34+
return new self;
35+
}
36+
37+
public function logSessionStart(string $class, string $method, string $namespace): void
38+
{
39+
$this->log(self::SESSION_START, $class, $method, $namespace);
40+
}
41+
42+
public function logSessionModify(string $class, string $method, string $namespace, string $key)
43+
{
44+
$this->log(self::SESSION_MODIFY, $class, $method, $namespace, $key);
45+
}
46+
47+
public function logControllerAction(string $class, string $method)
48+
{
49+
$this->log(self::CONTROLLER_ACTION, $class, $method);
50+
}
51+
52+
public function logWriteClose(string $class, string $method)
53+
{
54+
$this->log(self::SESSION_WRITE_CLOSE, $class, $method);
55+
}
56+
57+
private function log(string $category, string $class, string $method, ...$args): void
58+
{
59+
$this->calls[] = $category;
60+
$this->callDetails[] = $class . '::' . $method . '(' . implode(',', $args) . ')';
61+
}
62+
63+
public function asString(): string
64+
{
65+
return implode("\n", $this->callDetails);
66+
}
67+
68+
public function hasSessionStartAfterAction(): bool
69+
{
70+
$actionPosition = array_search(self::CONTROLLER_ACTION, $this->calls);
71+
$sessionStartPositions = array_keys($this->calls, self::SESSION_START);
72+
if (empty($sessionStartPositions)) {
73+
return false;
74+
}
75+
return max($sessionStartPositions) > $actionPosition;
76+
}
77+
78+
public function hasSessionStartBeforeAction(): bool
79+
{
80+
$actionPosition = array_search(self::CONTROLLER_ACTION, $this->calls);
81+
$sessionStartPositions = array_keys($this->calls, self::SESSION_START);
82+
if (empty($sessionStartPositions)) {
83+
return false;
84+
}
85+
return min($sessionStartPositions) < $actionPosition;
86+
}
87+
88+
public function hasSessionWriteCloseBeforeAction(): bool
89+
{
90+
$actionPosition = array_search(self::CONTROLLER_ACTION, $this->calls);
91+
$writeClosePositions = array_keys($this->calls, self::SESSION_WRITE_CLOSE);
92+
if (empty($writeClosePositions)) {
93+
return false;
94+
}
95+
return max($writeClosePositions) < $actionPosition;
96+
}
97+
98+
public function hasModifyAfterSessionWriteClose()
99+
{
100+
$writeClosePosition = array_search(self::SESSION_WRITE_CLOSE, $this->calls);
101+
$modifyPositions = array_keys($this->calls, self::SESSION_MODIFY);
102+
if (empty($modifyPositions)) {
103+
return false;
104+
}
105+
return max($modifyPositions) > $writeClosePosition;
106+
}
107+
108+
public function reset(): void
109+
{
110+
$this->calls = [];
111+
$this->callDetails = [];
112+
}
113+
}

Test/Util/SectionLoadActionSpy.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Test\Util;
5+
6+
use Magento\Customer\Controller\Section\Load;
7+
8+
class SectionLoadActionSpy extends Load
9+
{
10+
public function execute()
11+
{
12+
MethodLog::instance()->logControllerAction(parent::class, __FUNCTION__);
13+
return parent::execute();
14+
}
15+
16+
}

Test/Util/SessionSpy.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace IntegerNet\SessionUnblocker\Test\Util;
5+
6+
use Magento\Framework\Session\Generic;
7+
8+
class SessionSpy extends Generic
9+
{
10+
11+
public function writeClose()
12+
{
13+
MethodLog::instance()->logWriteClose(parent::class, __FUNCTION__);
14+
parent::writeClose();
15+
}
16+
17+
}

etc/di.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<?xml version="1.0" ?>
22
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
3+
<type name="Magento\Framework\Session\StorageInterface">
4+
<plugin name="integernet_session_unblocker" type="IntegerNet\SessionUnblocker\Plugin\SessionStoragePlugin" />
5+
</type>
6+
37
<type name="Magento\Customer\Controller\Section\Load">
48
<plugin name="integernet_session_unblocker" type="IntegerNet\SessionUnblocker\Plugin\SectionLoadControllerPlugin" />
59
</type>

0 commit comments

Comments
 (0)