API State setting within session, make tmpdb optional

- Allow limited state setting when session is already in progress
- Allow test sessions without a test database
- Denote an “in progress” session through a “testsession.started” session flag rather than the usage of a temporary database
This commit is contained in:
Ingo Schommer 2013-12-10 18:53:26 +01:00
parent c901abe83c
commit 72b48f4c38
8 changed files with 169 additions and 137 deletions

View File

@ -34,6 +34,7 @@ and interact with it through other URL endpoints.
Commands: Commands:
* `dev/testsession`: Shows options for starting a test session
* `dev/testsession/start`: Sets up test state, most commonly a test database will be constructed, * `dev/testsession/start`: Sets up test state, most commonly a test database will be constructed,
and your browser session will be amended to use this database. See "Parameters" documentation below. and your browser session will be amended to use this database. See "Parameters" documentation below.
* `dev/testsession/end`: Removes the test state, and resets to the original database. * `dev/testsession/end`: Removes the test state, and resets to the original database.
@ -45,14 +46,15 @@ Parameters for "dev/testsession/start":
* `fixture`: Loads a YAML fixture in the format generally accepted by `SapphireTest` * `fixture`: Loads a YAML fixture in the format generally accepted by `SapphireTest`
(see [fixture format docs](http://doc.silverstripe.org/framework/en/topics/testing/fixtures)). (see [fixture format docs](http://doc.silverstripe.org/framework/en/topics/testing/fixtures)).
The path should be relative to the webroot. The path should be relative to the webroot.
* `createDatabase`: Create a temporary database.
* `database`: Set an alternative database name in the current * `database`: Set an alternative database name in the current
browser session as a cookie. Does not actually create the database, browser session as a cookie. Does not actually create the database,
that's usually handled by `SapphireTest::create_temp_db()`. that's usually handled by `SapphireTest::create_temp_db()`.
Note: The database names are limited to a specific naming convention as a security measure: Note: The database names are limited to a specific naming convention as a security measure:
The "ss_tmpdb" prefix and a random sequence of seven digits. The "ss_tmpdb" prefix and a random sequence of seven digits.
This avoids the user gaining access to other production databases available on the same connection. This avoids the user gaining access to other production databases available on the same connection.
* `mailer`: Subclass of `Mailer`, typically used to record emails instead of actually sending them. * `mailer`: Subclass of `Mailer`, typically used to record emails instead of actually sending them.
* `date`: Sets a simulated date used for all framework operations. * `datetime`: Sets a simulated date used for all framework operations.
Format as "yyyy-MM-dd HH:mm:ss" (Example: "2012-12-31 18:40:59"). Format as "yyyy-MM-dd HH:mm:ss" (Example: "2012-12-31 18:40:59").
Example usage with parameters: Example usage with parameters:

View File

@ -1,19 +0,0 @@
<?php
if(
Config::inst()->get('Security', 'token')
&& DB::get_alternative_database_name()
) {
require_once BASE_PATH . '/vendor/autoload.php';
// Register mailer
if($mailer = Session::get('testsession.mailer')) {
Email::set_mailer(new $mailer());
\Config::inst()->update("Email","send_all_emails_to", null);
}
// Set mock date and time
$date = Session::get('testsession.date');
if($date) {
SS_Datetime::set_mock_now($date);
}
}

View File

@ -10,7 +10,8 @@ class TestSessionController extends Controller {
'set', 'set',
'end', 'end',
'clear', 'clear',
'Form', 'StartForm',
'ProgressForm',
); );
private static $alternative_database_name = -1; private static $alternative_database_name = -1;
@ -28,62 +29,88 @@ class TestSessionController extends Controller {
public function Link($action = null) { public function Link($action = null) {
return Controller::join_links(Director::baseUrl(), 'dev/testsession', $action); return Controller::join_links(Director::baseUrl(), 'dev/testsession', $action);
} }
public function index() {
if(Session::get('testsession.started')) {
return $this->renderWith('TestSession_inprogress');
} else {
return $this->renderWith('TestSession_start');
}
}
/** /**
* Start a test session. * Start a test session.
*/ */
public function start($request) { public function start() {
if(SapphireTest::using_temp_db()) return $this->renderWith('TestSession_inprogress'); $this->extend('onBeforeStart');
$params = $this->request->requestVars();
// Database if(isset($params['createDatabase'])) $params['createDatabase'] = 1; // legacy default behaviour
$dbName = $request->requestVar('database'); $this->setState($params);
if($dbName) { $this->extend('onAfterStart');
$dbExists = (bool)DB::query(
sprintf("SHOW DATABASES LIKE '%s'", Convert::raw2sql($dbName))
)->value();
} else {
$dbExists = false;
}
$this->extend('onBeforeStart', $dbName);
if(!$dbExists) {
// Create a new one with a randomized name
$dbName = SapphireTest::create_temp_db();
}
$this->setState(array_merge($request->requestVars(), array('database' => $dbName)));
$this->extend('onAfterStart', $dbName);
return $this->renderWith('TestSession_start'); return $this->renderWith('TestSession_inprogress');
} }
public function Form() { public function StartForm() {
$fields = new FieldList(
new CheckboxField('createDatabase', 'Create temporary database?', 1)
);
$fields->merge($this->getBaseFields());
$form = new Form( $form = new Form(
$this, $this,
'Form', 'StartForm',
$fields,
new FieldList( new FieldList(
(new TextField('fixture', 'Fixture YAML file path')) new FormAction('start', 'Start Session')
->setAttribute('placeholder', 'Example: framework/tests/security/MemberTest.yml'), )
$datetimeField = new DatetimeField('date', 'Custom date'), );
new HiddenField('flush', null, 1)
), $this->extend('updateStartForm', $form);
return $form;
}
/**
* Shows state which is allowed to be modified while a test session is in progress.
*/
public function ProgressForm() {
$fields = $this->getBaseFields();
$form = new Form(
$this,
'ProgressForm',
$fields,
new FieldList( new FieldList(
new FormAction('set', 'Set testing state') new FormAction('set', 'Set testing state')
) )
); );
$form->setFormAction($this->Link('set'));
$this->extend('updateProgressForm', $form);
return $form;
}
protected function getBaseFields() {
$fields = new FieldList(
(new TextField('fixture', 'Fixture YAML file path'))
->setAttribute('placeholder', 'Example: framework/tests/security/MemberTest.yml'),
$datetimeField = new DatetimeField('datetime', 'Custom date'),
new HiddenField('flush', null, 1)
);
$datetimeField->getDateField() $datetimeField->getDateField()
->setConfig('dateformat', 'yyyy-MM-dd') ->setConfig('dateformat', 'yyyy-MM-dd')
->setConfig('showcalendar', true) ->setConfig('showcalendar', true)
->setAttribute('placeholder', 'Date'); ->setAttribute('placeholder', 'Date (yyyy-MM-dd)');
$datetimeField->getTimeField() $datetimeField->getTimeField()
->setAttribute('placeholder', 'Time'); ->setConfig('timeformat', 'HH:mm:ss')
$form->setFormAction($this->Link('set')); ->setAttribute('placeholder', 'Time (HH:mm:ss)');
$datetimeField->setValue(Session::get('testsession.datetime'));
$this->extend('updateForm', $form); $this->extend('updateBaseFields', $fields);
return $form; return $fields;
} }
public function DatabaseName() { public function DatabaseName() {
@ -98,15 +125,12 @@ class TestSessionController extends Controller {
} }
} }
public function set($request) { public function set() {
if(!SapphireTest::using_temp_db()) { if(!Session::get('testsession.started')) {
throw new LogicException( throw new LogicException("No test session in progress.");
"This command can only be used with a temporary database. "
. "Perhaps you should use dev/testsession/start first?"
);
} }
$params = $request->requestVars(); $params = $this->request->requestVars();
$this->extend('onBeforeSet', $params); $this->extend('onBeforeSet', $params);
$this->setState($params); $this->setState($params);
$this->extend('onAfterSet'); $this->extend('onAfterSet');
@ -114,17 +138,16 @@ class TestSessionController extends Controller {
return $this->renderWith('TestSession_inprogress'); return $this->renderWith('TestSession_inprogress');
} }
public function clear($request) { public function clear() {
if(!SapphireTest::using_temp_db()) { if(!Session::get('testsession.started')) {
throw new LogicException( throw new LogicException("No test session in progress.");
"This command can only be used with a temporary database. "
. "Perhaps you should use dev/testsession/start first?"
);
} }
$this->extend('onBeforeClear'); $this->extend('onBeforeClear');
SapphireTest::empty_temp_db(); if(SapphireTest::using_temp_db()) {
SapphireTest::empty_temp_db();
}
if(isset($_SESSION['_testsession_codeblocks'])) { if(isset($_SESSION['_testsession_codeblocks'])) {
unset($_SESSION['_testsession_codeblocks']); unset($_SESSION['_testsession_codeblocks']);
@ -136,19 +159,19 @@ class TestSessionController extends Controller {
} }
public function end() { public function end() {
if(!SapphireTest::using_temp_db()) { if(!Session::get('testsession.started')) {
throw new LogicException( throw new LogicException("No test session in progress.");
"This command can only be used with a temporary database. "
. "Perhaps you should use dev/testsession/start first?"
);
} }
$this->extend('onBeforeEnd'); $this->extend('onBeforeEnd');
SapphireTest::kill_temp_db(); if(SapphireTest::using_temp_db()) {
DB::set_alternative_database_name(null); SapphireTest::kill_temp_db();
// Workaround for bug in Cookie::get(), fixed in 3.1-rc1 DB::set_alternative_database_name(null);
self::$alternative_database_name = null; // Workaround for bug in Cookie::get(), fixed in 3.1-rc1
self::$alternative_database_name = null;
}
Session::clear('testsession'); Session::clear('testsession');
$this->extend('onAfterEnd'); $this->extend('onAfterEnd');
@ -189,27 +212,48 @@ class TestSessionController extends Controller {
// Filter keys // Filter keys
$data = array_diff_key( $data = array_diff_key(
$data, $data,
array('action_set' => true, 'SecurityID' => true, 'url' => true) array(
'action_set' => true,
'action_start' => true,
'SecurityID' => true,
'url' => true,
'flush' => true,
)
); );
// Database // Database
$dbname = (isset($data['database'])) ? $data['database'] : null; if(
if($dbname) { !Session::get('testsession.started')
&& (@$data['createDatabase'] || @$data['database'])
) {
$dbName = (isset($data['database'])) ? $data['database'] : null;
if($dbName) {
$dbExists = (bool)DB::query(
sprintf("SHOW DATABASES LIKE '%s'", Convert::raw2sql($dbName))
)->value();
} else {
$dbExists = false;
}
if(!$dbExists) {
// Create a new one with a randomized name
$dbName = SapphireTest::create_temp_db();
}
// Set existing one, assumes it already has been created // Set existing one, assumes it already has been created
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_'; $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
$pattern = strtolower(sprintf('#^%stmpdb\d{7}#', $prefix)); $pattern = strtolower(sprintf('#^%stmpdb\d{7}#', $prefix));
if(!preg_match($pattern, $dbname)) { if(!preg_match($pattern, $dbName)) {
throw new InvalidArgumentException("Invalid database name format"); throw new InvalidArgumentException("Invalid database name format");
} }
DB::set_alternative_database_name($dbname); DB::set_alternative_database_name($dbName);
// Workaround for bug in Cookie::get(), fixed in 3.1-rc1 // Workaround for bug in Cookie::get(), fixed in 3.1-rc1
self::$alternative_database_name = $dbname; self::$alternative_database_name = $dbName;
// Database name is set in cookie (next request), ensure its available on this request already // Database name is set in cookie (next request), ensure its available on this request already
global $databaseConfig; global $databaseConfig;
DB::connect(array_merge($databaseConfig, array('database' => $dbname))); DB::connect(array_merge($databaseConfig, array('database' => $dbName)));
unset($data['database']); if(isset($data['database'])) unset($data['database']);
} }
// Fixtures // Fixtures
$fixtureFile = (isset($data['fixture'])) ? $data['fixture'] : null; $fixtureFile = (isset($data['fixture'])) ? $data['fixture'] : null;
@ -227,34 +271,33 @@ class TestSessionController extends Controller {
$mailer $mailer
)); ));
} }
// Configured through testsession/_config.php
Session::set('testsession.mailer', $mailer); Session::set('testsession.mailer', $mailer);
unset($data['mailer']); unset($data['mailer']);
} }
// Date // Date and time
$date = (isset($data['date'])) ? $data['date'] : null; if(@$data['datetime']['date'] && @$data['datetime']['time']) {
if($date) {
require_once 'Zend/Date.php'; require_once 'Zend/Date.php';
// Convert DatetimeField format // Convert DatetimeField format
if(is_array($date)) $date = $date['date'] . ' ' . $date['time']; $datetime = $data['datetime']['date'] . ' ' . $data['datetime']['time'];
if(!Zend_Date::isDate($date, 'yyyy-MM-dd HH:mm:ss')) { if(!Zend_Date::isDate($datetime, 'yyyy-MM-dd HH:mm:ss')) {
throw new LogicException(sprintf( throw new LogicException(sprintf(
'Invalid date format "%s", use yyyy-MM-dd HH:mm:ss', 'Invalid date format "%s", use yyyy-MM-dd HH:mm:ss',
$date $datetime
)); ));
} }
Session::set('testsession.datetime', $datetime);
// Configured through testsession/_config.php unset($data['datetime']);
Session::set('testsession.date', $date); } else {
unset($data['date']); unset($data['datetime']);
} }
// Set all other keys without special handling // Set all other keys without special handling
if($data) foreach($data as $k => $v) { if($data) foreach($data as $k => $v) {
Session::set('testsession.' . $k, $v); Session::set('testsession.' . $k, $v);
} }
Session::set('testsession.started', true);
} }
/** /**
@ -262,12 +305,10 @@ class TestSessionController extends Controller {
*/ */
public function getState() { public function getState() {
$state = array(); $state = array();
if($dbname = DB::get_alternative_database_name()) { $state[] = new ArrayData(array(
$state[] = new ArrayData(array(
'Name' => 'Database', 'Name' => 'Database',
'Value' => $dbname, 'Value' => DB::getConn()->currentDatabase(),
)); ));
}
$sessionStates = Session::get('testsession'); $sessionStates = Session::get('testsession');
if($sessionStates) foreach($sessionStates as $k => $v) { if($sessionStates) foreach($sessionStates as $k => $v) {
$state[] = new ArrayData(array( $state[] = new ArrayData(array(

View File

@ -1,14 +1,28 @@
<?php <?php
/** /**
* Allows inclusion of a PHP file, usually with procedural commands * Sets state previously initialized through {@link TestSessionController}.
* 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
* 'testsessio.stubfile' state parameter.
*/ */
class TestSessionRequestFilter { class TestSessionRequestFilter {
public function preRequest($req, $session, $model) { public function preRequest($req, $session, $model) {
if(!$session->inst_get('testsession.started')) return;
// Date and time
if($datetime = $session->inst_get('testsession.datetime')) {
SS_Datetime::set_mock_now($datetime);
}
// Register mailer
if($mailer = $session->inst_get('testsession.mailer')) {
Email::set_mailer(new $mailer());
Config::inst()->update("Email","send_all_emails_to", null);
}
// 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
// 'testsessio.stubfile' state parameter.
$file = $session->inst_get('testsession.stubfile'); $file = $session->inst_get('testsession.stubfile');
if(!Director::isLive() && $file && file_exists($file)) { if(!Director::isLive() && $file && file_exists($file)) {
// Connect to the database so the included code can interact with it // Connect to the database so the included code can interact with it

View File

@ -7,5 +7,4 @@
<% end_loop %> <% end_loop %>
</ul> </ul>
</p> </p>
<% end_if %> <% end_if %>
$Form

View File

@ -18,7 +18,7 @@
<a id="home-link" href="$BaseHref">Return to your site</a> <a id="home-link" href="$BaseHref">Return to your site</a>
</li> </li>
<li> <li>
<a id="start-link" href="$Link(start)">Start a new test session</a> <a id="start-link" href="$Link">Start a new test session</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -12,11 +12,25 @@
<h1>SilverStripe TestSession</h1> <h1>SilverStripe TestSession</h1>
</div> </div>
<div class="content"> <div class="content">
<!-- SUCCESS: DBNAME=$DatabaseName -->
<p> <p>
You're in the middle of a test session. Test session in progress.
<a id="end-session" href="$Link(end)">Click here to end it.</a> <a id="end-session" href="$Link(end)">Click here to end it.</a>
</p> </p>
<p>Where would you like to start?</p>
<ul>
<li>
<a id="home-link" href="$BaseHref">Homepage - published site</a>
</li>
<li>
<a id="draft-link" href="$BaseHref/?stage=Stage">Homepage - draft site</a>
</li>
<li>
<a id="admin-link" href="$BaseHref/admin/">CMS Admin</a>
</li>
</ul>
<% include TestSession_State %> <% include TestSession_State %>
$ProgressForm
</div> </div>
</body> </body>
</html> </html>

View File

@ -12,27 +12,8 @@
<h1>SilverStripe TestSession</h1> <h1>SilverStripe TestSession</h1>
</div> </div>
<div class="content"> <div class="content">
<!-- SUCCESS: DBNAME=$DatabaseName --> <p>Start a new test session</p>
<p> $StartForm
Started testing session.
<% if Fixture %>Loaded fixture "$Fixture" into database.<% end_if %>
Time to start testing; where would you like to start?
</p>
<ul>
<li>
<a id="home-link" href="$BaseHref">Homepage - published site</a>
</li>
<li>
<a id="draft-link" href="$BaseHref/?stage=Stage">Homepage - draft site</a>
</li>
<li>
<a id="admin-link" href="$BaseHref/admin/">CMS Admin</a>
</li>
<li>
<a id="end-link" href="$Link(end)">End your test session</a>
</li>
</ul>
<% include TestSession_State %>
</div> </div>
</body> </body>
</html> </html>