Compare commits

..

No commits in common. "2" and "2.0.0-alpha2" have entirely different histories.

28 changed files with 431 additions and 617 deletions

View File

@ -10,11 +10,8 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[{*.yml,*.json}] [{*.yml,package.json}]
indent_size = 2 indent_size = 2
[composer.json]
indent_size = 4
# The indent size used in the package.json file cannot be changed: # The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516 # https://github.com/npm/npm/pull/3180#issuecomment-16336516

View File

@ -1,11 +0,0 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
ci:
name: CI
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1

View File

@ -1,16 +0,0 @@
name: Dispatch CI
on:
# At 2:30 PM UTC, only on Monday and Tuesday
schedule:
- cron: '30 14 * * 1,2'
jobs:
dispatch-ci:
name: Dispatch CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Dispatch CI
uses: silverstripe/gha-dispatch-ci@v1

View File

@ -1,17 +0,0 @@
name: Keepalive
on:
workflow_dispatch:
# The 4th of every month at 10:50am UTC
schedule:
- cron: '50 10 4 * *'
jobs:
keepalive:
name: Keepalive
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Keepalive
uses: silverstripe/gha-keepalive@v1

69
.scrutinizer.yml Normal file
View File

@ -0,0 +1,69 @@
inherit: true
checks:
php:
verify_property_names: true
verify_argument_usable_as_reference: true
verify_access_scope_valid: true
useless_calls: true
use_statement_alias_conflict: true
variable_existence: true
unused_variables: true
unused_properties: true
unused_parameters: true
unused_methods: true
unreachable_code: true
too_many_arguments: true
sql_injection_vulnerabilities: true
simplify_boolean_return: true
side_effects_or_types: true
security_vulnerabilities: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
require_scope_for_properties: true
require_scope_for_methods: true
require_php_tag_first: true
psr2_switch_declaration: true
psr2_class_declaration: true
property_assignments: true
prefer_while_loop_over_for_loop: true
precedence_mistakes: true
precedence_in_conditions: true
phpunit_assertions: true
php5_style_constructor: true
parse_doc_comments: true
parameter_non_unique: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
optional_parameters_at_the_end: true
one_class_per_file: true
no_unnecessary_if: true
no_trailing_whitespace: true
no_property_on_interface: true
no_non_implemented_abstract_methods: true
no_error_suppression: true
no_duplicate_arguments: true
no_commented_out_code: true
newline_at_end_of_file: true
missing_arguments: true
method_calls_on_non_object: true
instanceof_class_exists: true
foreach_traversable: true
fix_line_ending: true
fix_doc_comments: true
duplication: true
deprecated_code_usage: true
deadlock_detection_in_loops: true
code_rating: true
closure_use_not_conflicting: true
catch_class_exists: true
blank_line_after_namespace_declaration: false
avoid_multiple_statements_on_same_line: true
avoid_duplicate_types: true
avoid_conflicting_incrementers: true
avoid_closing_tag: true
assignment_of_null_return: true
argument_type_checks: true
filter:
paths: [code/*, tests/*]

28
.travis.yml Normal file
View File

@ -0,0 +1,28 @@
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
language: php
sudo: false
php:
- 5.5
env:
matrix:
- DB=MYSQL CORE_RELEASE=master
matrix:
include:
- php: 5.6
env: DB=PGSQL CORE_RELEASE=master
- php: 5.6
env: DB=MYSQL CORE_RELEASE=master
before_script:
- phpenv rehash
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
script:
- vendor/bin/phpunit testsession/tests/unit/

View File

@ -1,7 +0,0 @@
mappings:
TestSessionController: SilverStripe\TestSession\TestSessionController
TestSessionEnvironment: SilverStripe\TestSession\TestSessionEnvironment
TestSessionRequestFilter: SilverStripe\TestSession\TestSessionHTTPMiddleware
TestSessionStubCodeWriter: SilverStripe\TestSession\TestSessionStubCodeWriter
excludedPaths:
- '*/testsession/_config/services.yml'

View File

@ -1,6 +1,6 @@
# Browser Test Session Module # Browser Test Session Module
[![CI](https://github.com/silverstripe/silverstripe-testsession/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-testsession/actions/workflows/ci.yml) [![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-testsession.svg)](https://travis-ci.org/silverstripe-labs/silverstripe-testsession)
## Overview ## Overview
@ -8,7 +8,7 @@
*It's completely possible to allow any user to become an admin, or do other nefarious things, if this is installed on a live site.* *It's completely possible to allow any user to become an admin, or do other nefarious things, if this is installed on a live site.*
This module starts a testing session in a browser, This module starts a testing session in a browser,
in order to test a Silverstripe application in a clean state. in order to test a SilverStripe application in a clean state.
Usually the session is started on a fresh database with only default records loaded. Usually the session is started on a fresh database with only default records loaded.
Further data can be loaded from YAML fixtures or database dumps. Further data can be loaded from YAML fixtures or database dumps.
@ -20,13 +20,8 @@ is a random token stored in the browser session, in order to make the
test session specific to the executing browser, and allow multiple test session specific to the executing browser, and allow multiple
people using their own test session in the same webroot. people using their own test session in the same webroot.
The module also keeps some metadata about the session state in the database,
so that it may be available for the clients as well.
E.g. the silverstripe-behat-extension may use it through this module APIs,
allowing us to introduce some grey-box testing techniques.
The module also serves as an initializer for the The module also serves as an initializer for the
[Silverstripe Behat Extension](https://github.com/silverstripe-labs/silverstripe-behat-extension/). [SilverStripe Behat Extension](https://github.com/silverstripe-labs/silverstripe-behat-extension/).
It is required for Behat because the Behat CLI test runner needs to persist It is required for Behat because the Behat CLI test runner needs to persist
test configuration just for the tested browser connection, test configuration just for the tested browser connection,
available on arbitary URL endpoints. For example, available on arbitary URL endpoints. For example,
@ -35,7 +30,7 @@ into a temporary database table for inspection by the CLI-based process.
## Setup ## Setup
Simply require the module in a Silverstripe webroot (3.0 or newer): Simply require the module in a SilverStripe webroot (3.0 or newer):
composer require --dev silverstripe/behat-extension composer require --dev silverstripe/behat-extension

View File

@ -1,27 +1,28 @@
<?php <?php
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\TestSession\TestSessionEnvironment; use SilverStripe\Core\Injector\Injector;
// Determine whether there is a testsession currently running, and if so - setup the persistent details for it. // Determine whether there is a testsession currently running, and if so - setup the persistent details for it.
TestSessionEnvironment::singleton()->loadFromFile(); Injector::inst()->get('TestSessionEnvironment')->loadFromFile();
/** /**
* This closure will run every time a Resque_Event is forked (just before it is forked, so it applies to the parent * This closure will run every time a Resque_Event is forked (just before it is forked, so it applies to the parent
* and child process). * and child process).
*/ */
if (class_exists('Resque_Event') && class_exists('SSResqueRun')) { if(class_exists('Resque_Event') && class_exists('SSResqueRun')) {
Resque_Event::listen('beforeFork', function ($data) { Resque_Event::listen('beforeFork', function($data) {
$databaseConfig = DB::getConfig(); global $databaseConfig;
// Reconnect to the database - this may connect to the old DB first, but is required because these processes // Reconnect to the database - this may connect to the old DB first, but is required because these processes
// are long-lived, and MySQL connections often get closed in between worker runs. We need to connect before // are long-lived, and MySQL connections often get closed in between worker runs. We need to connect before
// calling {@link TestSessionEnvironment::loadFromFile()}. // calling {@link TestSessionEnvironment::loadFromFile()}.
DB::connect($databaseConfig); DB::connect($databaseConfig);
$testEnv = TestSessionEnvironment::singleton(); $testEnv = Injector::inst()->get('TestSessionEnvironment');
if ($testEnv->isRunningTests()) { if($testEnv->isRunningTests()) {
$testEnv->loadFromFile(); $testEnv->loadFromFile();
} else { } else {
$testEnv->endTestSession(); $testEnv->endTestSession();

8
_config/_config.yml Normal file
View File

@ -0,0 +1,8 @@
---
Name: requestprocessors
---
Injector:
RequestProcessor:
properties:
filters:
- '%$TestSessionRequestFilter'

View File

@ -1,12 +0,0 @@
---
Name: testsessionprocessors
After:
- requestprocessors
Before:
- coresecurity
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director:
properties:
Middlewares:
TestSessionHTTPMiddleware: '%$SilverStripe\TestSession\TestSessionHTTPMiddleware'

View File

@ -1,7 +1,6 @@
--- ---
Name: testsessionroutes Name: testsessionroutes
--- ---
SilverStripe\Dev\DevelopmentAdmin: Director:
registered_controllers: rules:
testsession: 'dev/testsession': 'TestSessionController'
controller: SilverStripe\TestSession\TestSessionController

View File

@ -1,8 +0,0 @@
---
Name: testsessionservices
---
SilverStripe\Core\Injector\Injector:
SilverStripe\TestSession\TestSessionEnvironment:
class: SilverStripe\TestSession\TestSessionEnvironment
# shorthand alias for FQN
TestSessionEnvironment: '%$SilverStripe\TestSession\TestSessionEnvironment'

View File

@ -1,13 +1,12 @@
<?php <?php
namespace SilverStripe\TestSession;
use FilesystemIterator;
use LogicException;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\Session;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DatetimeField; use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\DropdownField;
@ -17,11 +16,9 @@ use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\RandomGenerator;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
@ -31,7 +28,6 @@ use SilverStripe\View\Requirements;
*/ */
class TestSessionController extends Controller class TestSessionController extends Controller
{ {
private static $url_segment = 'dev/testsession';
private static $allowed_actions = array( private static $allowed_actions = array(
'index', 'index',
@ -60,7 +56,7 @@ class TestSessionController extends Controller
{ {
parent::__construct(); parent::__construct();
$this->environment = TestSessionEnvironment::singleton(); $this->environment = Injector::inst()->get('TestSessionEnvironment');
} }
public function init() public function init()
@ -78,8 +74,13 @@ class TestSessionController extends Controller
return; return;
} }
Requirements::javascript('//code.jquery.com/jquery-1.7.2.min.js'); Requirements::javascript('framework/thirdparty/jquery/jquery.js');
Requirements::javascript('silverstripe/testsession:client/js/testsession.js'); Requirements::javascript('testsession/javascript/testsession.js');
}
public function Link($action = null)
{
return Controller::join_links(Director::baseURL(), 'dev/testsession', $action);
} }
public function index() public function index()
@ -98,14 +99,14 @@ class TestSessionController extends Controller
*/ */
public function start() public function start()
{ {
$params = $this->getRequest()->requestVars(); $params = $this->request->requestVars();
if (!empty($params['globalTestSession'])) { if (!empty($params['globalTestSession'])) {
$id = null; $id = null;
} else { } else {
$generator = Injector::inst()->get(RandomGenerator::class); $generator = Injector::inst()->get('SilverStripe\\Security\\RandomGenerator');
$id = substr($generator->randomToken() ?? '', 0, 10); $id = substr($generator->randomToken(), 0, 10);
$this->getRequest()->getSession()->set('TestSessionId', $id); Session::set('TestSessionId', $id);
} }
// Convert datetime from form object into a single string // Convert datetime from form object into a single string
@ -113,7 +114,7 @@ class TestSessionController extends Controller
// Remove unnecessary items of form-specific data from being saved in the test session // Remove unnecessary items of form-specific data from being saved in the test session
$params = array_diff_key( $params = array_diff_key(
$params ?? [], $params,
array( array(
'action_set' => true, 'action_set' => true,
'action_start' => true, 'action_start' => true,
@ -170,20 +171,19 @@ class TestSessionController extends Controller
throw new LogicException("No test session in progress."); throw new LogicException("No test session in progress.");
} }
$newSessionStates = array_diff_key($request->getVars() ?? [], array('url' => true)); $newSessionStates = array_diff_key($request->getVars(), array('url' => true));
if (!$newSessionStates) { if (!$newSessionStates) {
throw new LogicException('No query parameters detected'); throw new LogicException('No query parameters detected');
} }
$session = $this->getRequest()->getSession(); $sessionStates = (array)Session::get('_TestSessionController.BrowserSessionState');
$sessionStates = (array)$session->get('_TestSessionController.BrowserSessionState');
foreach ($newSessionStates as $k => $v) { foreach ($newSessionStates as $k => $v) {
$session->set($k, $v); Session::set($k, $v);
} }
// Track which state we're setting so we can unset later in end() // Track which state we're setting so we can unset later in end()
$session->set('_TestSessionController.BrowserSessionState', array_merge($sessionStates, $newSessionStates)); Session::set('_TestSessionController.BrowserSessionState', array_merge($sessionStates, $newSessionStates));
} }
public function StartForm() public function StartForm()
@ -255,6 +255,13 @@ class TestSessionController extends Controller
new HiddenField('flush', null, 1) new HiddenField('flush', null, 1)
); );
$textfield->setAttribute('placeholder', 'Example: framework/tests/security/MemberTest.yml'); $textfield->setAttribute('placeholder', 'Example: framework/tests/security/MemberTest.yml');
$datetimeField->getDateField()
->setConfig('dateformat', 'yyyy-MM-dd')
->setConfig('showcalendar', true)
->setAttribute('placeholder', 'Date (yyyy-MM-dd)');
$datetimeField->getTimeField()
->setConfig('timeformat', 'HH:mm:ss')
->setAttribute('placeholder', 'Time (HH:mm:ss)');
$datetimeField->setValue((isset($testState->datetime) ? $testState->datetime : null)); $datetimeField->setValue((isset($testState->datetime) ? $testState->datetime : null));
$this->extend('updateBaseFields', $fields); $this->extend('updateBaseFields', $fields);
@ -288,7 +295,7 @@ class TestSessionController extends Controller
// Remove unnecessary items of form-specific data from being saved in the test session // Remove unnecessary items of form-specific data from being saved in the test session
$params = array_diff_key( $params = array_diff_key(
$params ?? [], $params,
array( array(
'action_set' => true, 'action_set' => true,
'action_start' => true, 'action_start' => true,
@ -311,9 +318,8 @@ class TestSessionController extends Controller
$this->extend('onBeforeClear'); $this->extend('onBeforeClear');
$tempDB = new TempDatabase(); if (SapphireTest::using_temp_db()) {
if ($tempDB->isUsed()) { SapphireTest::empty_temp_db();
$tempDB->clearAllData();
} }
if (isset($_SESSION['_testsession_codeblocks'])) { if (isset($_SESSION['_testsession_codeblocks'])) {
@ -337,17 +343,17 @@ class TestSessionController extends Controller
} }
$this->environment->endTestSession(); $this->environment->endTestSession();
$session = Controller::curr()->getRequest()->getSession(); Session::clear('TestSessionId');
$session->clear('TestSessionId');
// Clear out all PHP session states which have been set previously // Clear out all PHP session states which have been set previously
if ($sessionStates = $session->get('_TestSessionController.BrowserSessionState')) { if ($sessionStates = Session::get('_TestSessionController.BrowserSessionState')) {
foreach ($sessionStates as $k => $v) { foreach ($sessionStates as $k => $v) {
$session->clear($k); Session::clear($k);
} }
$session->clear('_TestSessionController'); Session::clear('_TestSessionController');
} }
return $this->renderWith('TestSession_end'); return $this->renderWith('TestSession_end');
} }
@ -356,8 +362,13 @@ class TestSessionController extends Controller
*/ */
public function isTesting() public function isTesting()
{ {
$tempDB = new TempDatabase(); return SapphireTest::using_temp_db();
return $tempDB->isUsed(); }
public function setState($data)
{
Deprecation::notice('3.1', 'TestSessionController::setState() is no longer used, please use '
. 'TestSessionEnvironment instead.');
} }
/** /**
@ -391,7 +402,7 @@ class TestSessionController extends Controller
$templates = array(); $templates = array();
if (!$path) { if (!$path) {
$path = $this->config()->get('database_templates_path'); $path = $this->config()->database_templates_path;
} }
// TODO Remove once we can set BASE_PATH through the config layer // TODO Remove once we can set BASE_PATH through the config layer
@ -399,7 +410,7 @@ class TestSessionController extends Controller
$path = BASE_PATH . '/' . $path; $path = BASE_PATH . '/' . $path;
} }
if ($path && file_exists($path ?? '')) { if ($path && file_exists($path)) {
$it = new FilesystemIterator($path); $it = new FilesystemIterator($path);
foreach ($it as $fileinfo) { foreach ($it as $fileinfo) {
if ($fileinfo->getExtension() != 'sql') { if ($fileinfo->getExtension() != 'sql') {

View File

@ -1,27 +1,15 @@
<?php <?php
namespace SilverStripe\TestSession;
use DirectoryIterator;
use Exception;
use InvalidArgumentException;
use LogicException;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Environment;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Object;
use SilverStripe\Dev\FixtureFactory; use SilverStripe\Dev\FixtureFactory;
use SilverStripe\Dev\YamlFixture; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\Versioned\Versioned; use SilverStripe\ORM\Versioning\Versioned;
use stdClass;
/** /**
* Responsible for starting and finalizing test sessions. * Responsible for starting and finalizing test sessions.
@ -47,11 +35,8 @@ use stdClass;
* *
* See {@link $state} for default information stored in the test session. * See {@link $state} for default information stored in the test session.
*/ */
class TestSessionEnvironment class TestSessionEnvironment extends Object
{ {
use Injectable;
use Configurable;
use Extensible;
/** /**
* @var int Optional identifier for the session. * @var int Optional identifier for the session.
@ -80,30 +65,27 @@ class TestSessionEnvironment
public function __construct($id = null) public function __construct($id = null)
{ {
parent::__construct();
if ($id) { if ($id) {
$this->id = $id; $this->id = $id;
} } else {
} Session::start();
public function init(HTTPRequest $request)
{
if (!$this->id) {
$request->getSession()->init($request);
// $_SESSION != Session::get() in some execution paths, suspect Controller->pushCurrent() // $_SESSION != Session::get() in some execution paths, suspect Controller->pushCurrent()
// as part of the issue, easiest resolution is to use session directly for now // as part of the issue, easiest resolution is to use session directly for now
$this->id = $request->getSession()->get('TestSessionId'); $this->id = (isset($_SESSION['TestSessionId'])) ? $_SESSION['TestSessionId'] : null;
} }
} }
/** /**
* @return string Absolute path to the file persisting our state. * @return String Absolute path to the file persisting our state.
*/ */
public function getFilePath() public function getFilePath()
{ {
if ($this->id) { if ($this->id) {
$path = Director::getAbsFile(sprintf($this->config()->get('test_state_id_file') ?? '', $this->id)); $path = Director::getAbsFile(sprintf($this->config()->test_state_id_file, $this->id));
} else { } else {
$path = Director::getAbsFile($this->config()->get('test_state_file')); $path = Director::getAbsFile($this->config()->test_state_file);
} }
return $path; return $path;
@ -114,7 +96,7 @@ class TestSessionEnvironment
*/ */
public function isRunningTests() public function isRunningTests()
{ {
return (file_exists($this->getFilePath() ?? '')); return(file_exists($this->getFilePath()));
} }
/** /**
@ -160,13 +142,10 @@ class TestSessionEnvironment
// Convert to JSON and back so we can share the applyState() code between this and ->loadFromFile() // Convert to JSON and back so we can share the applyState() code between this and ->loadFromFile()
$json = json_encode($state, JSON_FORCE_OBJECT); $json = json_encode($state, JSON_FORCE_OBJECT);
$state = json_decode($json ?? ''); $state = json_decode($json);
$this->applyState($state); $this->applyState($state);
// Back up /assets folder
$this->backupAssets();
$this->extend('onAfterStartTestSession'); $this->extend('onAfterStartTestSession');
} }
@ -176,80 +155,13 @@ class TestSessionEnvironment
// Convert to JSON and back so we can share the appleState() code between this and ->loadFromFile() // Convert to JSON and back so we can share the appleState() code between this and ->loadFromFile()
$json = json_encode($state, JSON_FORCE_OBJECT); $json = json_encode($state, JSON_FORCE_OBJECT);
$state = json_decode($json ?? ''); $state = json_decode($json);
$this->applyState($state); $this->applyState($state);
$this->extend('onAfterUpdateTestSession'); $this->extend('onAfterUpdateTestSession');
} }
/**
* Backup all assets from /assets to /assets_backup.
* Note: Only does file move, no files ever duplicated / deleted
*/
protected function backupAssets()
{
// Ensure files backed up to assets dir
$backupFolder = $this->getAssetsBackupfolder();
if (!is_dir($backupFolder ?? '')) {
Filesystem::makeFolder($backupFolder);
}
$this->moveRecursive(ASSETS_PATH, $backupFolder, ['.htaccess', 'web.config', '.protected']);
}
/**
* Restore all assets to /assets folder.
* Note: Only does file move, no files ever duplicated / deleted
*/
public function restoreAssets()
{
// Ensure files backed up to assets dir
$backupFolder = $this->getAssetsBackupfolder();
if (is_dir($backupFolder ?? '')) {
// Move all files
Filesystem::makeFolder(ASSETS_PATH);
$this->moveRecursive($backupFolder, ASSETS_PATH);
Filesystem::removeFolder($backupFolder);
}
}
/**
* Recursively move files from one directory to another
*
* @param string $src Source of files being moved
* @param string $dest Destination of files being moved
* @param array $ignore List of files to not move
*/
protected function moveRecursive($src, $dest, $ignore = [])
{
// If source is not a directory stop processing
if (!is_dir($src ?? '')) {
return;
}
// If the destination directory does not exist create it
if (!is_dir($dest ?? '') && !mkdir($dest ?? '')) {
// If the destination directory could not be created stop processing
return;
}
// Open the source directory to read in files
$iterator = new DirectoryIterator($src);
foreach ($iterator as $file) {
if ($file->isFile()) {
if (!in_array($file->getFilename(), $ignore ?? [])) {
rename($file->getRealPath() ?? '', $dest . DIRECTORY_SEPARATOR . $file->getFilename());
}
} elseif (!$file->isDot() && $file->isDir()) {
// If a dir is ignored, still move children but don't remove self
$this->moveRecursive($file->getRealPath(), $dest . DIRECTORY_SEPARATOR . $file);
if (!in_array($file->getFilename(), $ignore ?? [])) {
Filesystem::removeFolder($file->getRealPath());
}
}
}
}
/** /**
* Assumes the database has already been created in startTestSession(), as this method can be called from * 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. * _config.php where we don't yet have a DB connection.
@ -268,7 +180,7 @@ class TestSessionEnvironment
$this->extend('onBeforeApplyState', $state); $this->extend('onBeforeApplyState', $state);
// back up source // back up source
$databaseConfig = DB::getConfig(); global $databaseConfig;
$this->oldDatabaseName = $databaseConfig['database']; $this->oldDatabaseName = $databaseConfig['database'];
// Load existing state from $this->state into $state, if there is any // Load existing state from $this->state into $state, if there is any
@ -283,7 +195,26 @@ class TestSessionEnvironment
} }
// ensure we have a connection to the database // ensure we have a connection to the database
$this->connectToDatabase($state); if (isset($state->database) && $state->database) {
if (!DB::get_conn()) {
// No connection, so try and connect to tmpdb if it exists
if (isset($state->database)) {
$this->oldDatabaseName = $databaseConfig['database'];
$databaseConfig['database'] = $state->database;
}
// Connect to database
DB::connect($databaseConfig);
} else {
// We've already connected to the database, do a fast check to see what database we're currently using
$db = DB::get_conn()->getSelectedDatabase();
if (isset($state->database) && $db != $state->database) {
$this->oldDatabaseName = $databaseConfig['database'];
$databaseConfig['database'] = $state->database;
DB::connect($databaseConfig);
}
}
}
// Database // Database
if (!$this->isRunningTests()) { if (!$this->isRunningTests()) {
@ -297,15 +228,14 @@ class TestSessionEnvironment
if (!$dbExists) { if (!$dbExists) {
// Create a new one with a randomized name // Create a new one with a randomized name
$tempDB = new TempDatabase(); $dbName = SapphireTest::create_temp_db();
$dbName = $tempDB->build();
$state->database = $dbName; // In case it's changed by the call to SapphireTest::create_temp_db(); $state->database = $dbName; // In case it's changed by the call to SapphireTest::create_temp_db();
// Set existing one, assumes it already has been created // Set existing one, assumes it already has been created
$prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_'; $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
$pattern = strtolower(sprintf('#^%stmpdb.*#', preg_quote($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");
} }
@ -315,15 +245,13 @@ class TestSessionEnvironment
// Connect to the new database, overwriting the old DB connection (if any) // Connect to the new database, overwriting the old DB connection (if any)
DB::connect($databaseConfig); DB::connect($databaseConfig);
} }
TestSessionState::create()->write(); // initialize the session state
} }
// Mailer // Mailer
$mailer = (isset($state->mailer)) ? $state->mailer : null; $mailer = (isset($state->mailer)) ? $state->mailer : null;
if ($mailer) { if ($mailer) {
if (!class_exists($mailer ?? '') || !is_subclass_of($mailer, 'SilverStripe\\Control\\Email\\Mailer')) { if (!class_exists($mailer) || !is_subclass_of($mailer, 'SilverStripe\\Control\\Email\\Mailer')) {
throw new InvalidArgumentException(sprintf( throw new InvalidArgumentException(sprintf(
'Class "%s" is not a valid class, or subclass of Mailer', 'Class "%s" is not a valid class, or subclass of Mailer',
$mailer $mailer
@ -333,10 +261,9 @@ class TestSessionEnvironment
// Date and time // Date and time
if (isset($state->datetime)) { if (isset($state->datetime)) {
$formatter = DBDatetime::singleton()->getFormatter(); require_once 'Zend/Date.php';
$formatter->setPattern(DBDatetime::ISO_DATETIME);
// Convert DatetimeField format // Convert DatetimeField format
if ($formatter->parse($state->datetime) === false) { if (!Zend_Date::isDate($state->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',
$state->datetime $state->datetime
@ -345,7 +272,6 @@ class TestSessionEnvironment
} }
$this->saveState($state); $this->saveState($state);
$this->extend('onAfterApplyState'); $this->extend('onAfterApplyState');
} }
@ -357,13 +283,14 @@ class TestSessionEnvironment
*/ */
public function importDatabase($path, $requireDefaultRecords = false) public function importDatabase($path, $requireDefaultRecords = false)
{ {
$sql = file_get_contents($path ?? ''); $sql = file_get_contents($path);
// Split into individual query commands, removing comments // Split into individual query commands, removing comments
$sqlCmds = array_filter(preg_split( $sqlCmds = array_filter(
'/;\n/', preg_split('/;\n/',
preg_replace(array('/^$\n/m', '/^(\/|#).*$\n/m'), '', $sql ?? '') ?? '' preg_replace(array('/^$\n/m', '/^(\/|#).*$\n/m'), '', $sql)
) ?? []); )
);
// Execute each query // Execute each query
foreach ($sqlCmds as $sqlCmd) { foreach ($sqlCmds as $sqlCmd) {
@ -400,7 +327,7 @@ class TestSessionEnvironment
$content = json_encode($state); $content = json_encode($state);
} }
$old = umask(0); $old = umask(0);
file_put_contents($this->getFilePath() ?? '', $content, LOCK_EX); file_put_contents($this->getFilePath(), $content, LOCK_EX);
umask($old); umask($old);
} }
@ -408,17 +335,14 @@ class TestSessionEnvironment
{ {
if ($this->isRunningTests()) { if ($this->isRunningTests()) {
try { try {
$contents = file_get_contents($this->getFilePath() ?? ''); $contents = file_get_contents($this->getFilePath());
$json = json_decode($contents ?? ''); $json = json_decode($contents);
$this->applyState($json); $this->applyState($json);
} catch (Exception $e) { } catch (Exception $e) {
throw new Exception( throw new \Exception("A test session appears to be in progress, but we can't retrieve the details. "
"A test session appears to be in progress, but we can't retrieve the details.\n" . "Try removing the " . $this->getFilePath() . " file. Inner "
. "Try removing the " . $this->getFilePath() . " file.\n" . "error: " . $e->getMessage());
. "Inner error: " . $e->getMessage() . "\n"
. "Stacktrace: " . $e->getTraceAsString()
);
} }
} }
} }
@ -427,8 +351,8 @@ class TestSessionEnvironment
{ {
$file = $this->getFilePath(); $file = $this->getFilePath();
if (file_exists($file ?? '')) { if (file_exists($file)) {
if (!unlink($file ?? '')) { if (!unlink($file)) {
throw new \Exception('Unable to remove the testsession state file, please remove it manually. File ' throw new \Exception('Unable to remove the testsession state file, please remove it manually. File '
. 'path: ' . $file); . 'path: ' . $file);
} }
@ -450,22 +374,19 @@ class TestSessionEnvironment
{ {
$this->extend('onBeforeEndTestSession'); $this->extend('onBeforeEndTestSession');
// Restore assets if (SapphireTest::using_temp_db()) {
$this->restoreAssets();
// Reset DB
$tempDB = new TempDatabase();
if ($tempDB->isUsed()) {
$state = $this->getState(); $state = $this->getState();
$dbConn = DB::get_schema(); $dbConn = DB::get_schema();
$dbExists = $dbConn->databaseExists($state->database); $dbExists = $dbConn->databaseExists($state->database);
if ($dbExists) { if($dbExists) {
// Clean up temp database // Clean up temp database
$dbConn->dropDatabase($state->database); $dbConn->dropDatabase($state->database);
file_put_contents('php://stdout', "Deleted temp database: $state->database" . PHP_EOL); file_put_contents('php://stdout', "Deleted temp database: $state->database" . PHP_EOL);
} }
// End test session mode // End test session mode
$this->resetDatabaseName(); $this->resetDatabaseName();
SapphireTest::set_is_running_test(false);
} }
$this->removeStateFile(); $this->removeStateFile();
@ -482,20 +403,20 @@ class TestSessionEnvironment
*/ */
public function loadFixtureIntoDb($fixtureFile) public function loadFixtureIntoDb($fixtureFile)
{ {
$realFile = realpath(BASE_PATH . '/' . $fixtureFile); $realFile = realpath(BASE_PATH.'/'.$fixtureFile);
$baseDir = realpath(Director::baseFolder() ?? ''); $baseDir = realpath(Director::baseFolder());
if (!$realFile || !file_exists($realFile ?? '')) { if (!$realFile || !file_exists($realFile)) {
throw new LogicException("Fixture file doesn't exist"); throw new LogicException("Fixture file doesn't exist");
} elseif (substr($realFile ?? '', 0, strlen($baseDir ?? '')) != $baseDir) { } elseif (substr($realFile, 0, strlen($baseDir)) != $baseDir) {
throw new LogicException("Fixture file must be inside $baseDir"); throw new LogicException("Fixture file must be inside $baseDir");
} elseif (substr($realFile ?? '', -4) != '.yml') { } elseif (substr($realFile, -4) != '.yml') {
throw new LogicException("Fixture file must be a .yml file"); throw new LogicException("Fixture file must be a .yml file");
} elseif (!preg_match('/^([^\/.][^\/]+)\/tests\//', $fixtureFile ?? '')) { } elseif (!preg_match('/^([^\/.][^\/]+)\/tests\//', $fixtureFile)) {
throw new LogicException("Fixture file must be inside the tests subfolder of one of your modules."); throw new LogicException("Fixture file must be inside the tests subfolder of one of your modules.");
} }
$factory = Injector::inst()->create(FixtureFactory::class); $factory = Injector::inst()->create('SilverStripe\\Dev\\FixtureFactory');
$fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); $fixture = Injector::inst()->create('SilverStripe\\Dev\\YamlFixture', $fixtureFile);
$fixture->writeInto($factory); $fixture->writeInto($factory);
$state = $this->getState(); $state = $this->getState();
@ -511,9 +432,9 @@ class TestSessionEnvironment
public function resetDatabaseName() public function resetDatabaseName()
{ {
if ($this->oldDatabaseName) { if ($this->oldDatabaseName) {
$databaseConfig = DB::getConfig(); global $databaseConfig;
$databaseConfig['database'] = $this->oldDatabaseName; $databaseConfig['database'] = $this->oldDatabaseName;
DB::setConfig($databaseConfig);
$conn = DB::get_conn(); $conn = DB::get_conn();
@ -529,85 +450,6 @@ class TestSessionEnvironment
public function getState() public function getState()
{ {
$path = Director::getAbsFile($this->getFilePath()); $path = Director::getAbsFile($this->getFilePath());
if (file_exists($path ?? '')) { return (file_exists($path)) ? json_decode(file_get_contents($path)) : new stdClass;
return json_decode(file_get_contents($path)) ?: new stdClass;
}
return new stdClass;
}
/**
* Path where assets should be backed up during testing
*
* @return string
*/
protected function getAssetsBackupfolder()
{
return PUBLIC_PATH . DIRECTORY_SEPARATOR . 'assets_backup';
}
/**
* Ensure that there is a connection to the database
*
* @param mixed $state
*/
public function connectToDatabase($state = null)
{
if ($state == null) {
$state = $this->getState();
}
$databaseConfig = DB::getConfig();
if (isset($state->database) && $state->database) {
if (!DB::get_conn()) {
// No connection, so try and connect to tmpdb if it exists
if (isset($state->database)) {
$this->oldDatabaseName = $databaseConfig['database'];
$databaseConfig['database'] = $state->database;
}
// Connect to database
DB::connect($databaseConfig);
} else {
// We've already connected to the database, do a fast check to see what database we're currently using
$db = DB::get_conn()->getSelectedDatabase();
if (isset($state->database) && $db != $state->database) {
$this->oldDatabaseName = $databaseConfig['database'];
$databaseConfig['database'] = $state->database;
DB::connect($databaseConfig);
}
}
}
}
/**
* Wait for pending requests
*
* @param int $await Time to wait (in ms) after the last response (to allow the browser react)
* @param int $timeout For how long (in ms) do we wait before giving up
*
* @return bool Whether there are no more pending requests
*/
public function waitForPendingRequests($await = 700, $timeout = 10000)
{
$timeout = TestSessionState::millitime() + $timeout;
$interval = max(300, $await);
do {
$now = TestSessionState::millitime();
if ($timeout < $now) {
return false;
}
$model = TestSessionState::get()->byID(1);
$pendingRequests = $model->PendingRequests > 0;
$lastRequestAwait = ($model->LastResponseTimestamp + $await) > $now;
$pending = $pendingRequests || $lastRequestAwait;
} while ($pending && (usleep($interval * 1000) || true));
return true;
} }
} }

View File

@ -0,0 +1,81 @@
<?php
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\DB;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Director;
use SilverStripe\Control\RequestFilter;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/**
* Sets state previously initialized through {@link TestSessionController}.
*/
class TestSessionRequestFilter implements RequestFilter
{
/**
* @var TestSessionEnvironment
*/
protected $testSessionEnvironment;
public function __construct()
{
$this->testSessionEnvironment = Injector::inst()->get('TestSessionEnvironment');
}
public function preRequest(HTTPRequest $request, Session $session, DataModel $model)
{
if (!$this->testSessionEnvironment->isRunningTests()) {
return;
}
$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(), 'SilverStripe\\Control\\Email\\Mailer');
Config::inst()->update("SilverStripe\\Control\\Email\\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
// 'testsession.stubfile' state parameter.
if (isset($testState->stubfile)) {
$file = $testState->stubfile;
if (!Director::isLive() && $file && file_exists($file)) {
// Connect to the database so the included code can interact with it
global $databaseConfig;
if ($databaseConfig) {
DB::connect($databaseConfig);
}
include_once($file);
}
}
}
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model)
{
if (!$this->testSessionEnvironment->isRunningTests()) {
return;
}
// Store PHP session
$state = $this->testSessionEnvironment->getState();
$state->session = Session::get_all();
$this->testSessionEnvironment->applyState($state);
}
}

View File

@ -1,13 +1,11 @@
<?php <?php
namespace SilverStripe\TestSession;
/** /**
* Writes PHP to a file which can be included in SilverStripe runs on existence. * Writes PHP to a file which can be included in SilverStripe runs on existence.
* The generated file is included in page execution through {@link TestSessionRequestFilter}. * The generated file is included in page execution through {@link TestSessionRequestFilter}.
*/ */
class TestSessionStubCodeWriter class TestSessionStubCodeWriter
{ {
/** /**
* @var boolean Add debug statements to the generated PHP about * @var boolean Add debug statements to the generated PHP about
* the generator's origin code location. * the generator's origin code location.
@ -39,8 +37,8 @@ class TestSessionStubCodeWriter
$header = ''; $header = '';
// Create file incl. header if it doesn't exist // Create file incl. header if it doesn't exist
if (!file_exists($this->getFilePath() ?? '')) { if (!file_exists($this->getFilePath())) {
touch($this->getFilePath() ?? ''); touch($this->getFilePath());
if ($this->debug) { if ($this->debug) {
$header .= "<?php\n// Generated by " . $trace[1]['class'] . " on " . date('Y-m-d H:i:s') . "\n\n"; $header .= "<?php\n// Generated by " . $trace[1]['class'] . " on " . date('Y-m-d H:i:s') . "\n\n";
} else { } else {
@ -52,13 +50,13 @@ class TestSessionStubCodeWriter
if ($this->debug) { if ($this->debug) {
$header .= "// Added by " . $trace[1]['class'] . '::' . $trace[1]['function'] . "\n"; $header .= "// Added by " . $trace[1]['class'] . '::' . $trace[1]['function'] . "\n";
} }
file_put_contents($path ?? '', $header . $php . "\n", FILE_APPEND); file_put_contents($path, $header . $php . "\n", FILE_APPEND);
} }
public function reset() public function reset()
{ {
if (file_exists($this->getFilePath() ?? '')) { if (file_exists($this->getFilePath())) {
unlink($this->getFilePath() ?? ''); unlink($this->getFilePath());
} }
} }

View File

@ -1,13 +1,10 @@
{ {
"name": "silverstripe/testsession", "name": "silverstripe/testsession",
"type": "silverstripe-vendormodule", "type": "silverstripe-module",
"description": "Support module for browser-based test sessions, e.g. for Behat behaviour testing", "description": "Support module for browser-based test sessions, e.g. for Behat behaviour testing",
"homepage": "http://silverstripe.org", "homepage": "http://silverstripe.org",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"keywords": [ "keywords": ["silverstripe", "testing"],
"silverstripe",
"testing"
],
"authors": [ "authors": [
{ {
"name": "SilverStripe", "name": "SilverStripe",
@ -16,24 +13,11 @@
], ],
"require": { "require": {
"composer/installers": "*", "composer/installers": "*",
"silverstripe/framework": "^4@dev", "silverstripe/framework": "^4"
"silverstripe/vendor-plugin": "^1.3"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.5"
}, },
"extra": { "extra": {
"expose": [ "branch-alias": {
"client" "dev-master": "2.0.x-dev"
]
},
"scripts": {
"lint": "phpcs -s src/ tests/"
},
"autoload": {
"psr-4": {
"SilverStripe\\TestSession\\": "src/",
"SilverStripe\\TestSession\\Tests\\": "tests/"
} }
}, },
"minimum-stability": "dev" "minimum-stability": "dev"

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="SilverStripe">
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
<file>src</file>
<!-- base rules are PSR-2 -->
<rule ref="PSR2" >
<!-- Current exclusions -->
<exclude name="PSR1.Methods.CamelCapsMethodName" />
</rule>
</ruleset>

View File

@ -1,98 +0,0 @@
<?php
namespace SilverStripe\TestSession;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
/**
* Sets state previously initialized through {@link TestSessionController}.
*/
class TestSessionHTTPMiddleware implements HTTPMiddleware
{
/**
* @var TestSessionEnvironment
*/
protected $testSessionEnvironment;
public function __construct()
{
$this->testSessionEnvironment = TestSessionEnvironment::singleton();
}
public function process(HTTPRequest $request, callable $delegate)
{
// Init environment
$this->testSessionEnvironment->init($request);
// If not running tests, just pass through
$isRunningTests = $this->testSessionEnvironment->isRunningTests();
if (!$isRunningTests) {
return $delegate($request);
}
// Load test state
$this->loadTestState($request);
TestSessionState::incrementState();
// Call with safe teardown
try {
return $delegate($request);
} finally {
$this->restoreTestState($request);
TestSessionState::decrementState();
}
}
/**
* Load test state from environment into "real" environment
*
* @param HTTPRequest $request
*/
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);
Email::config()->set('admin_email', 'no-reply@example.com');
}
// 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);
}
}
}
protected function restoreTestState(HTTPRequest $request)
{
// Store PHP session
$state = $this->testSessionEnvironment->getState();
$state->session = $request->getSession()->getAll();
$this->testSessionEnvironment->applyState($state);
}
}

View File

@ -1,70 +0,0 @@
<?php
namespace SilverStripe\TestSession;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Queries\SQLUpdate;
/**
* The session state keeps some metadata about the current test session.
* This may allow the client (Behat) to get some insight into the
* server side affairs (e.g. if the server is handling some number requests at the moment).
*
* The client side (Behat) must not use this class straightforwardly, but rather
* rely on the API of {@see TestSessionEnvironment} or {@see TestSessionController}.
*
* @property int PendingRequests keeps information about how many requests are in progress
* @property float LastResponseTimestamp microtime of the last response made by the server
*/
class TestSessionState extends DataObject
{
private static $table_name = 'TestSessionState';
private static $db = [
'PendingRequests' => 'Int',
'LastResponseTimestamp' => 'Decimal(14, 0)'
];
/**
* Increments TestSessionState.PendingRequests number by 1
* to indicate we have one more request in progress
*/
public static function incrementState()
{
$schema = DataObject::getSchema();
$update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(self::class)))
->addWhere(['ID' => 1])
->assignSQL('"PendingRequests"', '"PendingRequests" + 1');
$update->execute();
}
/**
* Decrements TestSessionState.PendingRequests number by 1
* to indicate we have one more request in progress.
* Also updates TestSessionState.LastResponseTimestamp
* to the current timestamp.
*/
public static function decrementState()
{
$schema = DataObject::getSchema();
$update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(self::class)))
->addWhere(['ID' => 1])
->assignSQL('"PendingRequests"', '"PendingRequests" - 1')
->assign('"LastResponseTimestamp"', self::millitime());
$update->execute();
}
/**
* Returns unix timestamp in milliseconds
*
* @return float milliseconds since 1970
*/
public static function millitime()
{
return round(microtime(true) * 1000);
}
}

View File

@ -1,8 +1,8 @@
<% if $State %> <% if State %>
<p> <p>
<a href="#" onclick="document.getElementById('state').style.display = 'block'; return false;">Show testing state</a> <a href="#" onclick="document.getElementById('state').style.display = 'block'; return false;">Show testing state</a>
<ul id="state" style="display: none;"> <ul id="state" style="display: none;">
<% loop $State %> <% loop State %>
<li><strong>$Name:</strong> $Value</li> <li><strong>$Name:</strong> $Value</li>
<% end_loop %> <% end_loop %>
</ul> </ul>

View File

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<% base_tag %> <% base_tag %>
$MetaTags $MetaTags
<% require css('silverstripe/framework:client/styles/debug.css') %> <% require css('framework/css/debug.css') %>
<% require css('silverstripe/testsession:client/styles/styles.css') %> <% require css('testsession/css/styles.css') %>
</head> </head>
<body> <body>
<div class="info"> <div class="info">

View File

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<% base_tag %> <% base_tag %>
$MetaTags $MetaTags
<% require css('silverstripe/framework:client/styles/debug.css') %> <% require css('framework/css/debug.css') %>
<% require css('silverstripe/testsession:client/styles/styles.css') %> <% require css('testsession/css/styles.css') %>
</head> </head>
<body> <body>
<div class="info"> <div class="info">

View File

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<% base_tag %> <% base_tag %>
$MetaTags $MetaTags
<% require css('silverstripe/framework:client/styles/debug.css') %> <% require css('framework/css/debug.css') %>
<% require css('silverstripe/testsession:client/styles/styles.css') %> <% require css('testsession/css/styles.css') %>
</head> </head>
<body> <body>
<div class="info"> <div class="info">

View File

@ -0,0 +1,52 @@
<?php
use SilverStripe\Dev\SapphireTest;
class TestSessionStubCodeWriterTest extends SapphireTest
{
public function tearDown()
{
parent::tearDown();
$file = TEMP_FOLDER . '/TestSessionStubCodeWriterTest-file.php';
if (file_exists($file)) {
unlink($file);
}
}
public function testWritesHeaderOnNewFile()
{
$file = TEMP_FOLDER . '/TestSessionStubCodeWriterTest-file.php';
$writer = new TestSessionStubCodeWriter($file);
$writer->write('foo();', false);
$this->assertFileExists($file);
$this->assertEquals(
file_get_contents($writer->getFilePath()),
"<?php\nfoo();\n"
);
}
public function testWritesWithAppendOnExistingFile()
{
$file = TEMP_FOLDER . '/TestSessionStubCodeWriterTest-file.php';
$writer = new TestSessionStubCodeWriter($file);
$writer->write('foo();', false);
$writer->write('bar();', false);
$this->assertFileExists($file);
$this->assertEquals(
file_get_contents($writer->getFilePath()),
"<?php\nfoo();\nbar();\n"
);
}
public function testReset()
{
$file = TEMP_FOLDER . '/TestSessionStubCodeWriterTest-file.php';
$writer = new TestSessionStubCodeWriter($file);
$writer->write('foo();', false);
$this->assertFileExists($file);
$writer->reset();
$this->assertFileNotExists($file);
}
}