Merge pull request #65 from blueo/pulls/prevent-recreating-session-file

Prevent re-creating session file on end
This commit is contained in:
Serge Latyntsev 2019-06-21 10:41:25 +12:00 committed by GitHub
commit f7632a8df4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 645 additions and 86 deletions

View File

@ -311,7 +311,7 @@ class TestSessionController extends Controller
$this->extend('onBeforeClear');
$tempDB = new TempDatabase();
$tempDB = TempDatabase::create();
if ($tempDB->isUsed()) {
$tempDB->clearAllData();
}

View File

@ -7,10 +7,12 @@ use Exception;
use InvalidArgumentException;
use LogicException;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Environment;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
@ -20,6 +22,7 @@ use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\TestSession\TestSessionState;
use SilverStripe\Versioned\Versioned;
use stdClass;
@ -162,7 +165,9 @@ class TestSessionEnvironment
$json = json_encode($state, JSON_FORCE_OBJECT);
$state = json_decode($json);
$this->applyState($state);
$state = $this->createDatabase($state);
$state = $this->applyState($state);
$this->saveState($state);
// Back up /assets folder
$this->backupAssets();
@ -179,6 +184,7 @@ class TestSessionEnvironment
$state = json_decode($json);
$this->applyState($state);
$this->saveState($state);
$this->extend('onAfterUpdateTestSession');
}
@ -251,42 +257,15 @@ class TestSessionEnvironment
}
/**
* Assumes the database has already been created in startTestSession(), as this method can be called from
* _config.php where we don't yet have a DB connection.
* Creates a TempDB
* Will modify state if the created database has a different name from what was in state
*
* Persists the state to the filesystem.
*
* You can extend this by creating an Extension object and implementing either onBeforeApplyState() or
* onAfterApplyState() to add your own test state handling in.
*
* @param mixed $state
* @throws LogicException
* @throws InvalidArgumentException
* @param stdClass $state
* @return stdClass $state
*/
public function applyState($state)
public function createDatabase($state)
{
$this->extend('onBeforeApplyState', $state);
// back up source
$databaseConfig = DB::getConfig();
$this->oldDatabaseName = $databaseConfig['database'];
// Load existing state from $this->state into $state, if there is any
$oldState = $this->getState();
if ($oldState) {
foreach ($oldState as $k => $v) {
if (!isset($state->$k)) {
$state->$k = $v; // Don't overwrite stuff in $state, as that's the new state
}
}
}
// ensure we have a connection to the database
$this->connectToDatabase($state);
// Database
if (!$this->isRunningTests()) {
$dbCreate = isset($state->createDatabase) ? (bool) $state->createDatabase : false;
$dbName = (isset($state->database)) ? $state->database : null;
if ($dbName) {
@ -295,9 +274,11 @@ class TestSessionEnvironment
$dbExists = false;
}
if (!$dbExists) {
$databaseConfig = DB::getConfig();
if (!$dbExists && $dbCreate) {
$this->oldDatabaseName = $databaseConfig['database'];
// Create a new one with a randomized name
$tempDB = new TempDatabase();
$tempDB = TempDatabase::create();
$dbName = $tempDB->build();
$state->database = $dbName; // In case it's changed by the call to SapphireTest::create_temp_db();
@ -308,27 +289,57 @@ class TestSessionEnvironment
if (!preg_match($pattern, $dbName)) {
throw new InvalidArgumentException("Invalid database name format");
}
$this->oldDatabaseName = $databaseConfig['database'];
$databaseConfig['database'] = $dbName; // Instead of calling DB::set_alternative_db_name();
// Connect to the new database, overwriting the old DB connection (if any)
DB::connect($databaseConfig);
}
if ($dbExists || $dbCreate) {
// Connect to the new database, overwriting the old DB connection (if any)
$databaseConfig['database'] = $dbName; // Instead of calling DB::set_alternative_db_name();
DB::connect($databaseConfig);
TestSessionState::create()->write(); // initialize the session state
}
return $state;
}
/**
* Takes a state object and applies environment transformations to current application environment
*
* Does not persist any changes to the the state object - @see saveState for persistence
*
* Assumes the database has already been created in startTestSession(), as this method can be called from
* _config.php where we don't yet have a DB connection.
*
* You can extend this by creating an Extension object and implementing either onBeforeApplyState() or
* onAfterApplyState() to add your own test state handling in.
*
* @param mixed $state
* @throws LogicException
* @throws InvalidArgumentException
* @return mixed $state
*/
public function applyState($state)
{
$this->extend('onBeforeApplyState', $state);
// back up source
$databaseConfig = DB::getConfig();
$this->oldDatabaseName = $databaseConfig['database'];
// ensure we have a connection to the database
$this->connectToDatabase($state);
// Mailer
$mailer = (isset($state->mailer)) ? $state->mailer : null;
if ($mailer) {
if (!class_exists($mailer) || !is_subclass_of($mailer, 'SilverStripe\\Control\\Email\\Mailer')) {
if (!class_exists($mailer) || !is_subclass_of($mailer, Mailer::class)) {
throw new InvalidArgumentException(sprintf(
'Class "%s" is not a valid class, or subclass of Mailer',
$mailer
));
}
Injector::inst()->registerService(new $mailer(), Mailer::class);
Email::config()->set("send_all_emails_to", null);
}
// Date and time
@ -342,11 +353,24 @@ class TestSessionEnvironment
$state->datetime
));
}
DBDatetime::set_mock_now($state->datetime);
}
$this->saveState($state);
// Allows inclusion of a PHP file, usually with procedural commands
// to set up required test state. The file can be generated
// through {@link TestSessionStubCodeWriter}, and the session state
// set through {@link TestSessionController->set()} and the
// 'testsession.stubfile' state parameter.
if (isset($state->stubfile)) {
$file = $state->stubfile;
if (!Director::isLive() && $file && file_exists($file)) {
include_once($file);
}
}
$this->extend('onAfterApplyState');
$this->extend('onAfterApplyState', $state);
return $state;
}
/**
@ -454,7 +478,7 @@ class TestSessionEnvironment
$this->restoreAssets();
// Reset DB
$tempDB = new TempDatabase();
$tempDB = TempDatabase::create();
if ($tempDB->isUsed()) {
$state = $this->getState();
$dbConn = DB::get_schema();
@ -476,6 +500,8 @@ class TestSessionEnvironment
/**
* Loads a YAML fixture into the database as part of the {@link TestSessionController}.
*
* Writes loaded fixture files to $state->fixtures
*
* @param string $fixtureFile The .yml file to load
* @return FixtureFactory The loaded fixture
* @throws LogicException
@ -500,7 +526,7 @@ class TestSessionEnvironment
$state = $this->getState();
$state->fixtures[] = $fixtureFile;
$this->applyState($state);
$this->saveState($state);
return $fixture;
}
@ -546,8 +572,10 @@ class TestSessionEnvironment
* Ensure that there is a connection to the database
*
* @param mixed $state
* @return void
*/
public function connectToDatabase($state = null) {
public function connectToDatabase($state = null)
{
if ($state == null) {
$state = $this->getState();
}

View File

@ -57,41 +57,26 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware
*/
protected function loadTestState(HTTPRequest $request)
{
$testState = $this->testSessionEnvironment->getState();
// Date and time
if (isset($testState->datetime)) {
DBDatetime::set_mock_now($testState->datetime);
}
// Register mailer
if (isset($testState->mailer)) {
$mailer = $testState->mailer;
Injector::inst()->registerService(new $mailer(), Mailer::class);
Email::config()->set("send_all_emails_to", null);
}
// Connect to the test session database
$this->testSessionEnvironment->connectToDatabase();
// Allows inclusion of a PHP file, usually with procedural commands
// to set up required test state. The file can be generated
// through {@link TestSessionStubCodeWriter}, and the session state
// set through {@link TestSessionController->set()} and the
// 'testsession.stubfile' state parameter.
if (isset($testState->stubfile)) {
$file = $testState->stubfile;
if (!Director::isLive() && $file && file_exists($file)) {
include_once($file);
}
}
$state = $this->testSessionEnvironment->getState();
$this->testSessionEnvironment->applyState($state);
}
/**
* @param HTTPRequest $request
* @return void
*/
protected function restoreTestState(HTTPRequest $request)
{
// Store PHP session
$state = $this->testSessionEnvironment->getState();
$state->session = $request->getSession()->getAll();
$this->testSessionEnvironment->applyState($state);
// skip saving file if the session is being closed (all test properties are removed except session)
$keys = get_object_vars($state);
if (count($keys) <= 1) {
return;
}
$this->testSessionEnvironment->saveState($state);
}
}

View File

3
tests/stubs/teststub.php Normal file
View File

@ -0,0 +1,3 @@
<?php
define('TESTSESSION_STUBFILE', false);

View File

@ -0,0 +1,278 @@
<?php
namespace SilverStripe\TestSession\Tests\Unit;
use DateTime;
use LogicException;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\TestSession\TestSessionController;
use SilverStripe\TestSession\TestSessionEnvironment;
use SilverStripe\TestSession\TestSessionHTTPMiddleware;
use stdClass;
use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\Core\Config\Config;
class TestSessionControllerTest extends SapphireTest
{
/**
* @var TestSessionEnvironment|PHPUnit_Framework_MockObject_MockObject
*/
private $testSessionEnvironment;
protected $usesDatabase = true;
protected function setUp()
{
parent::setUp();
Injector::inst()->unregisterNamedObject(TestSessionEnvironment::class);
Injector::inst()->registerService(
$this->createMock(TestSessionEnvironment::class),
TestSessionEnvironment::class
);
$this->testSessionEnvironment = TestSessionEnvironment::singleton();
}
public function testIndex()
{
$controller = new TestSessionController();
$this->assertContains('Start a new test session', (string) $controller->index());
$env = $this->testSessionEnvironment;
$env->method('isRunningTests')
->willReturn(true);
$state = new stdClass();
$state->showme = 'test';
$env->method('getState')
->willReturn($state);
$html = (string) $controller->index();
$this->assertContains('Test session in progress.', $html);
$this->assertContains('showme:', $html);
}
public function testStartNonGlobalImport()
{
DBDatetime::set_mock_now(1552525373);
$params = [
'datetime' => [
'date' => DBDatetime::now()->Date(),
],
'importDatabasePath' => '/somepath',
'importDatabaseFilename' => 'bigOldb.sql',
'requireDefaultRecords' => '1',
'fixture' => 'fixture.yml',
];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$env = $this->testSessionEnvironment;
$state = new stdClass();
$env->method('getState')
->willReturn($state);
$env->expects($this->once())
->method('startTestSession')
->with(
[
'datetime' => 'Mar 14, 2019 00:00:00',
'importDatabasePath' => '/somepath',
'importDatabaseFilename' => 'bigOldb.sql',
'requireDefaultRecords' => '1',
'fixture' => 'fixture.yml',
],
$this->isType('string')
);
// cannot do an import and default records
$env->expects($this->never())
->method('requireDefaultRecords');
$env->expects($this->once())
->method('loadFixtureIntoDb')
->with('fixture.yml');
$html = (string) $controller->start();
$this->assertContains('Test session in progress.', $html);
$this->assertNotNull($request->getSession()->get('TestSessionId'));
}, 'dev/testsession/start', $params);
DBDatetime::clear_mock_now();
}
public function testStartNonGlobalDefaultRecords()
{
$params = [
'requireDefaultRecords' => '1',
];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$env = $this->testSessionEnvironment;
$state = new stdClass();
$env->method('getState')
->willReturn($state);
$env->expects($this->once())
->method('startTestSession')
->with(
[
'requireDefaultRecords' => '1',
],
$this->isType('string')
);
$env->expects($this->once())
->method('requireDefaultRecords')
->with();
$controller->start();
}, 'dev/testsession/start', $params);
}
public function testSetNotRunning()
{
$this->expectException(LogicException::class);
$params = [];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$controller->set();
}, 'dev/testsession/set', $params);
}
public function testSetRunning()
{
DBDatetime::set_mock_now(1552525373);
$params = [
'datetime' => [
'date' => DBDatetime::now()->Date(),
],
];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$env = $this->testSessionEnvironment;
$state = new stdClass();
$env->method('isRunningTests')
->willReturn(true);
$env->method('getState')
->willReturn($state);
$env->expects($this->once())
->method('updateTestSession')
->with(
[
'datetime' => 'Mar 14, 2019 00:00:00',
]
);
$html = (string) $controller->set();
$this->assertContains('Test session in progress.', $html);
}, 'dev/testsession/set', $params);
DBDatetime::clear_mock_now();
}
public function testClearNotRunning()
{
$this->expectException(LogicException::class);
$params = [];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$controller->clear();
}, 'dev/testsession/clear', $params);
}
public function testClearRunning()
{
$params = [];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$env = $this->testSessionEnvironment;
$state = new stdClass();
$env->method('isRunningTests')
->willReturn(true);
$env->method('getState')
->willReturn($state);
Config::nest();
Config::modify()->set(TempDatabase::class, 'factory', new class {
public function create()
{
$mockdb = $this->createMock(TempDatabase::class);
$mockdb->method('isUsed')->willReturn(true);
$mockdb->expects($this->once())
->method('clearAllData');
return $mockdb;
}
});
$msg = $controller->clear();
Config::unnest();
$this->assertEquals('Cleared database and test state', $msg);
}, 'dev/testsession/clear', $params);
}
public function testEndNotRunning()
{
$this->expectException(LogicException::class);
$params = [];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$controller->end();
}, 'dev/testsession/end', $params);
}
public function testEndRunning()
{
$params = [];
Director::mockRequest(function ($request) {
$controller = new TestSessionController();
$controller->setRequest($request);
$env = $this->testSessionEnvironment;
$state = new stdClass();
$env->method('isRunningTests')
->willReturn(true);
$env->method('getState')
->willReturn($state);
$env->expects($this->once())
->method('endTestSession');
$html = (string) $controller->end();
$this->assertContains('Test session ended', $html);
$this->assertNull($request->getSession()->get('TestSessionId'));
}, 'dev/testsession/end', $params);
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace SilverStripe\TestSession\Tests\Unit;
use DateTime;
use SilverStripe\BehatExtension\Utility\TestMailer;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\TestSession\TestSessionEnvironment;
use SilverStripe\TestSession\TestSessionState;
use stdClass;
class TestSessionEnvironmentTest extends SapphireTest
{
protected function setUp()
{
parent::setUp();
Injector::inst()->unregisterNamedObject(TestSessionEnvironment::class);
Injector::inst()->registerService(
new TestSessionEnvironment,
TestSessionEnvironment::class
);
$this->testSessionEnvironment = TestSessionEnvironment::singleton();
}
public function testApplyStateNoModification()
{
$state = new stdClass();
$state->testprop = '';
$env = new TestSessionEnvironment();
$afterApply = $env->applyState($state);
$this->assertSame($state, $afterApply);
}
public function testApplyStateDBConnect()
{
$state = new stdClass();
$state->testprop = '';
$env = $this->getMockBuilder(TestSessionEnvironment::class)
->setMethodsExcept(['applyState'])
->getMock();
$env->expects($this->once())
->method('connectToDatabase');
$env->applyState($state);
}
public function testApplyStateDatetime()
{
$state = new stdClass();
$now = new DateTime();
$now->modify('-1 day');
$state->datetime = $now->format('Y-m-d H:i:s');
$env = new TestSessionEnvironment();
$env->applyState($state);
$date = new DateTime();
$this->assertNotEquals($date->getTimestamp(), DBDatetime::now()->getTimestamp());
$this->assertEquals($now->getTimestamp(), DBDatetime::now()->getTimestamp());
$state->datetime = 'invalid format';
$this->expectExceptionMessage('Invalid date format "invalid format", use yyyy-MM-dd HH:mm:ss');
$env->applyState($state);
}
public function testAppyStateMailer()
{
$state = new stdClass();
$state->mailer = TestMailer::class;
$env = new TestSessionEnvironment();
$env->applyState($state);
$mailer = Injector::inst()->get(Mailer::class);
$this->assertEquals(TestMailer::class, get_class($mailer));
$state->mailer = 'stdClass';
$this->expectExceptionMessage('Class "stdClass" is not a valid class, or subclass of Mailer');
$env->applyState($state);
}
public function testAppyStateStub()
{
$module = ModuleLoader::getModule('silverstripe/testsession');
$state = new stdClass();
$state->stubfile = $module->getPath() . DIRECTORY_SEPARATOR . 'tests/stubs/teststub.php';
$env = new TestSessionEnvironment();
$this->assertFalse(defined('TESTSESSION_STUBFILE'));
$env->applyState($state);
$this->assertTrue(defined('TESTSESSION_STUBFILE'));
}
public function testConnectToDatabase()
{
$env = new TestSessionEnvironment();
$dbExisting = DB::get_conn()->getSelectedDatabase();
$env->connectToDatabase(new stdClass);
$db = DB::get_conn()->getSelectedDatabase();
$this->assertEquals($dbExisting, $db);
$tempDB = new TempDatabase();
$dbName = $tempDB->build();
$state = new stdClass();
$state->database = $dbName;
$env->connectToDatabase($state);
$db = DB::get_conn()->getSelectedDatabase();
$this->assertEquals($dbName, $db);
$tempDB->kill();
}
public function testCreateDatabase()
{
$env = new TestSessionEnvironment();
$dbExisting = DB::get_conn()->getSelectedDatabase();
$state = new stdClass;
$env->createDatabase($state);
$db = DB::get_conn()->getSelectedDatabase();
$this->assertEquals($dbExisting, $db);
$state = new stdClass();
$state->createDatabase = true;
$env->createDatabase($state);
$db = DB::get_conn()->getSelectedDatabase();
$this->assertNotEquals($dbExisting, $db);
// check the TestSessionState object has been created
$stateObj = TestSessionState::get()->first();
$this->assertNotNull($stateObj);
$this->assertTrue($stateObj->exists());
$tempDB = new TempDatabase('default');
$tempDB->kill();
$state = new stdClass();
$state->database = $dbExisting;
$env->connectToDatabase($state);
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace SilverStripe\TestSession\Tests\Unit;
use DateTime;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\TestSession\TestSessionEnvironment;
use SilverStripe\TestSession\TestSessionHTTPMiddleware;
use stdClass;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Core\Manifest\ModuleLoader;
class TestSessionHTTPMiddlewareTest extends SapphireTest
{
/**
* @var TestSessionEnvironment
*/
private $testSessionEnvironment;
protected $usesDatabase = true;
protected function setUp()
{
parent::setUp();
Injector::inst()->registerService(
$this->createMock(TestSessionEnvironment::class),
TestSessionEnvironment::class
);
$this->testSessionEnvironment = TestSessionEnvironment::singleton();
}
public function testProcessNoTestRunning()
{
$env = $this->testSessionEnvironment;
// setup expected calls on environment
$env->expects($this->never())
->method('getState');
$env->expects($this->never())
->method('applyState');
Injector::nest();
Injector::inst()->registerService(
$env,
TestSessionEnvironment::class
);
$middleware = new TestSessionHTTPMiddleware();
// Mock request
$session = new Session([]);
$request = new HTTPRequest('GET', '/');
$request->setSession($session);
$delegate = function () {
// noop
};
$middleware->process($request, $delegate);
Injector::unnest();
}
public function testProcessTestsRunning()
{
$state = new stdClass();
$env = $this->testSessionEnvironment;
$env->method('isRunningTests')
->willReturn(true);
// setup expected calls on environment
$env->expects($this->exactly(2))
->method('getState')
->willReturn($state);
$env->expects($this->once())
->method('applyState');
Injector::nest();
Injector::inst()->registerService(
$env,
TestSessionEnvironment::class
);
$middleware = new TestSessionHTTPMiddleware();
// Mock request
$session = new Session([]);
$request = new HTTPRequest('GET', '/');
$request->setSession($session);
$delegate = function () {
// noop
};
$middleware->process($request, $delegate);
Injector::unnest();
}
}