Compare commits

...

40 Commits
2.0.0 ... 2

Author SHA1 Message Date
Maxime Rainville ee49a440fb
Merge pull request #86 from creative-commoners/pulls/2/dispatch-ci
MNT Use gha-dispatch-ci
2023-03-23 12:16:16 +13:00
Steve Boyd cb34aa869f MNT Use gha-dispatch-ci 2023-03-21 12:19:24 +13:00
Steve Boyd 5c8070044c Merge branch '2.4' into 2 2023-02-02 16:20:55 +13:00
Maxime Rainville 8e3f71afb7
Merge pull request #85 from creative-commoners/pulls/2.4/null-state
FIX Handle invalid json data
2023-01-26 13:15:33 +13:00
Steve Boyd fe5e87598f FIX Handle invalid json data 2023-01-24 14:38:01 +13:00
Sabina Talipova ffe1bc8fa2
Merge pull request #83 from creative-commoners/pulls/2/stop-depr
API Stop using deprecated API
2022-11-11 13:08:01 +13:00
Steve Boyd 132c00f122 API Stop using deprecated API 2022-11-03 18:11:29 +13:00
Steve Boyd 9b91b2de98 Merge branch '2.3' into 2 2022-08-02 19:06:18 +12:00
Steve Boyd ecac4295af Merge branch '2.2' into 2.3 2022-08-02 19:05:52 +12:00
Guy Sartorelli c4786cd955
Merge pull request #77 from creative-commoners/pulls/2.2/standardise-modules
MNT Standardise modules
2022-08-02 14:43:58 +12:00
Steve Boyd ffe829485e MNT Standardise modules 2022-08-01 15:39:57 +12:00
Steve Boyd fc63ebe8a9 Merge branch '2.3' into 2 2022-07-25 11:46:50 +12:00
Steve Boyd 8535a680ca Merge branch '2.2' into 2.3 2022-07-25 11:46:22 +12:00
Guy Sartorelli 8db444605a
MNT Fix linting issues (#76) 2022-07-18 13:18:35 +12:00
Guy Sartorelli cefce74559
Merge pull request #75 from creative-commoners/pulls/2.0/module-standards
MNT Use GitHub Actions CI
2022-07-18 10:24:31 +12:00
Steve Boyd a1e8643ea7 MNT Use GitHub Actions CI 2022-07-18 09:55:08 +12:00
Guy Sartorelli 0f5cb30743
Merge pull request #73 from creative-commoners/pulls/2/php81
ENH PHP 8.1 compatibility
2022-04-22 16:16:50 +12:00
Steve Boyd 7733cc7c95 Merge branch '2.3' into 2 2022-04-13 18:14:53 +12:00
Steve Boyd 22b9ca23cb
Merge pull request #74 from creative-commoners/pulls/2.3/add-missing-email-config
FIX Add missing config to fix silverstipe/admin tests.
2022-04-13 18:13:46 +12:00
Steve Boyd 5e2ef7e52c ENH PHP 8.1 compatibility 2022-04-13 17:40:59 +12:00
Guy Sartorelli 44e5364ec2 FIX Add missing config to fix silverstipe/admin tests. 2022-04-13 17:11:57 +12:00
Ingo Schommer e44774dbf0
Fixed composer require instruction 2020-07-27 17:30:49 +12:00
Serge Latyntsev 0c479ad2eb
Merge pull request #63 from blueo/pulls/db-connection-on-state-load
Connect to test database on session load
2019-05-10 14:15:03 +12:00
Guy Marriott efb6777ee9
BUGFIX: updated route config for testsession endpoint (#66)
BUGFIX: updated route config for testsession endpoint
2019-05-08 16:27:55 +12:00
pjayme 81c2417414 updated route config for testsession endpoint 2019-05-08 12:05:28 +12:00
Bernard Hamlin 075d960e5d Connect to test database on session load 2019-03-13 09:17:12 +13:00
Serge Latyntsev 61d12ec08a
Merge pull request #61 from webbuilders-group/db-reconnect-fix
BUGFIX: Fixed issue where the incorrect database connection could be made when using a stubfile
2019-02-01 14:49:01 +13:00
UndefinedOffset e957d1e0fd BUGFIX: Fixed issue where the incorrect database connection could be made when using a stubfile (fixes #60) 2019-01-24 15:22:29 -04:00
Serge Latyntsev 8827e97417
Merge pull request #59 from open-sausages/pulls/2.2/wait-for-pending-requests-for-real
Fix TestSessionState and TestSessionEnvironment
2019-01-10 16:24:15 +13:00
Serge Latyntcev f54baefb5a Rename TestSessionState::microtime to millitime 2019-01-10 15:30:39 +13:00
Serge Latyntcev 32c8e6a3b1 Fix TestSessionState and TestSessionEnvironment
Fixing a bug that makes it only wait for pending requests, but
not for some time after the last response
2019-01-10 11:48:59 +13:00
Maxime Rainville fc0f7baa11
Merge pull request #58 from open-sausages/pulls/2.2/pending-requests-awaited
ADD / TestSessionState initial implementation
2019-01-08 17:35:17 +13:00
Serge Latyntcev 0c078e5027 TestSessionState implementation refinement;
Move increment/decrement methods to TestSessionState class,
fix some documentation, fix some code style and readability issues
2019-01-08 16:47:37 +13:00
Serge Latyntcev 78dd43ed96 ADD / TestSessionState initial implementation
TestSessionState model initial implementation
TestSessionEnvironment to initialize the state for every scenario and provide API for the clients to use it
TestSessionHTTPMiddleware to keep the state fields up to date
2018-12-19 14:28:38 +13:00
Damian Mooyman e6c9817328
Update 2 branch alias 2018-03-20 15:07:34 +13:00
Daniel Hensby 961069473c
Merge pull request #56 from open-sausages/pulls/2/fix-assets
BUG Prevent assets folder being destroyed on behat tests
2018-03-07 11:40:40 +00:00
Damian Mooyman 2e25beb703
BUG Prevent assets folder being destroyed on behat tests
ENHANCEMENT Shift into vendormodule
2018-03-07 14:34:56 +13:00
Damian Mooyman 2c277b53fb
Update branch alias for 2.x-dev to 2.1.x-dev 2017-11-28 13:20:08 +13:00
Damian Mooyman 5413f9182e
Merge branch '2.0' into 2 2017-11-28 13:19:29 +13:00
Damian Mooyman 3a65d766c7
Remove branch-alias for 2.0 branch 2017-11-28 13:19:02 +13:00
17 changed files with 385 additions and 190 deletions

View File

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

11
.github/workflows/ci.yml vendored Normal file
View File

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

16
.github/workflows/dispatch-ci.yml vendored Normal file
View File

@ -0,0 +1,16 @@
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

17
.github/workflows/keepalive.yml vendored Normal file
View File

@ -0,0 +1,17 @@
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

View File

@ -1,69 +0,0 @@
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/*]

View File

@ -1,6 +1,6 @@
# Browser Test Session Module
[![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-testsession.svg)](https://travis-ci.org/silverstripe-labs/silverstripe-testsession)
[![CI](https://github.com/silverstripe/silverstripe-testsession/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-testsession/actions/workflows/ci.yml)
## 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.*
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.
Further data can be loaded from YAML fixtures or database dumps.
@ -20,8 +20,13 @@ is a random token stored in the browser session, in order to make the
test session specific to the executing browser, and allow multiple
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
[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
test configuration just for the tested browser connection,
available on arbitary URL endpoints. For example,
@ -30,7 +35,7 @@ into a temporary database table for inspection by the CLI-based process.
## 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
@ -42,7 +47,7 @@ and interact with it through other URL endpoints.
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.
* `dev/testsession/end`: Removes the test state, and resets to the original database.
* `dev/testsession/loadfixture?fixture=<path>`: Loads a fixture into an existing test state.
@ -54,15 +59,15 @@ While you can use the interface to set the test session state,
it can be useful to set them programmatically through query parameters
on "dev/testsession/start":
* `fixture`: Loads a YAML fixture in the format generally accepted by `SapphireTest`
(see [fixture format docs](http://doc.silverstripe.org/en/developer_guides/testing/fixtures/)).
* `fixture`: Loads a YAML fixture in the format generally accepted by `SapphireTest`
(see [fixture format docs](http://doc.silverstripe.org/en/developer_guides/testing/fixtures/)).
The path should be relative to the webroot.
* `createDatabase`: Create a temporary database.
* `importDatabasePath`: Absolute path to a database dump to load into a newly created temporary database.
* `importDatabaseFilename`: File name for a database dump to load, relative to `TestSessionController.database_templates_path`
* `requireDefaultRecords`: Include default records as defined on the model classes (in PHP)
* `database`: Set an alternative database name in the current
browser session as a cookie. Does not actually create the database,
* `database`: Set an alternative database name in the current
browser session as a cookie. Does not actually create the database,
that's usually handled by `SapphireTest::create_temp_db()`.
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.
@ -71,9 +76,9 @@ on "dev/testsession/start":
* `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").
* `globalTestSession`: Activate test session independently of the current browser session,
effectively setting the site into test session mode for all users across different browsers.
Only available in "dev" mode. For example, create a global test session in Chrome, then you can share
the session data in Firefox. But if you have started a non-global session in a browser before starting
effectively setting the site into test session mode for all users across different browsers.
Only available in "dev" mode. For example, create a global test session in Chrome, then you can share
the session data in Firefox. But if you have started a non-global session in a browser before starting
a global session somewhere else, that non-global session will take priority in that browser.
Example usage with parameters:

View File

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

View File

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

View File

@ -2,6 +2,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 -->

View File

@ -31,6 +31,7 @@ use SilverStripe\View\Requirements;
*/
class TestSessionController extends Controller
{
private static $url_segment = 'dev/testsession';
private static $allowed_actions = array(
'index',
@ -77,13 +78,8 @@ class TestSessionController extends Controller
return;
}
Requirements::javascript('http://code.jquery.com/jquery-1.7.2.min.js');
Requirements::javascript('testsession/client/js/testsession.js');
}
public function Link($action = null)
{
return Controller::join_links(Director::baseURL(), 'dev/testsession', $action);
Requirements::javascript('//code.jquery.com/jquery-1.7.2.min.js');
Requirements::javascript('silverstripe/testsession:client/js/testsession.js');
}
public function index()
@ -108,7 +104,7 @@ class TestSessionController extends Controller
$id = null;
} else {
$generator = Injector::inst()->get(RandomGenerator::class);
$id = substr($generator->randomToken(), 0, 10);
$id = substr($generator->randomToken() ?? '', 0, 10);
$this->getRequest()->getSession()->set('TestSessionId', $id);
}
@ -117,7 +113,7 @@ class TestSessionController extends Controller
// Remove unnecessary items of form-specific data from being saved in the test session
$params = array_diff_key(
$params,
$params ?? [],
array(
'action_set' => true,
'action_start' => true,
@ -174,7 +170,7 @@ class TestSessionController extends Controller
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) {
throw new LogicException('No query parameters detected');
}
@ -292,7 +288,7 @@ class TestSessionController extends Controller
// Remove unnecessary items of form-specific data from being saved in the test session
$params = array_diff_key(
$params,
$params ?? [],
array(
'action_set' => true,
'action_start' => true,
@ -403,7 +399,7 @@ class TestSessionController extends Controller
$path = BASE_PATH . '/' . $path;
}
if ($path && file_exists($path)) {
if ($path && file_exists($path ?? '')) {
$it = new FilesystemIterator($path);
foreach ($it as $fileinfo) {
if ($fileinfo->getExtension() != 'sql') {

View File

@ -2,9 +2,11 @@
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\HTTPRequest;
@ -78,7 +80,6 @@ class TestSessionEnvironment
public function __construct($id = null)
{
$this->constructExtensions();
if ($id) {
$this->id = $id;
}
@ -100,7 +101,7 @@ class TestSessionEnvironment
public function getFilePath()
{
if ($this->id) {
$path = Director::getAbsFile(sprintf($this->config()->get('test_state_id_file'), $this->id));
$path = Director::getAbsFile(sprintf($this->config()->get('test_state_id_file') ?? '', $this->id));
} else {
$path = Director::getAbsFile($this->config()->get('test_state_file'));
}
@ -113,7 +114,7 @@ class TestSessionEnvironment
*/
public function isRunningTests()
{
return(file_exists($this->getFilePath()));
return (file_exists($this->getFilePath() ?? ''));
}
/**
@ -159,10 +160,13 @@ class TestSessionEnvironment
// Convert to JSON and back so we can share the applyState() code between this and ->loadFromFile()
$json = json_encode($state, JSON_FORCE_OBJECT);
$state = json_decode($json);
$state = json_decode($json ?? '');
$this->applyState($state);
// Back up /assets folder
$this->backupAssets();
$this->extend('onAfterStartTestSession');
}
@ -172,13 +176,80 @@ class TestSessionEnvironment
// Convert to JSON and back so we can share the appleState() code between this and ->loadFromFile()
$json = json_encode($state, JSON_FORCE_OBJECT);
$state = json_decode($json);
$state = json_decode($json ?? '');
$this->applyState($state);
$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
* _config.php where we don't yet have a DB connection.
@ -212,26 +283,7 @@ class TestSessionEnvironment
}
// ensure we have a connection to the database
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);
}
}
}
$this->connectToDatabase($state);
// Database
if (!$this->isRunningTests()) {
@ -252,8 +304,8 @@ class TestSessionEnvironment
// Set existing one, assumes it already has been created
$prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
$pattern = strtolower(sprintf('#^%stmpdb.*#', preg_quote($prefix, '#')));
if (!preg_match($pattern, $dbName)) {
$pattern = strtolower(sprintf('#^%stmpdb.*#', preg_quote($prefix ?? '', '#')));
if (!preg_match($pattern ?? '', $dbName ?? '')) {
throw new InvalidArgumentException("Invalid database name format");
}
@ -263,13 +315,15 @@ class TestSessionEnvironment
// Connect to the new database, overwriting the old DB connection (if any)
DB::connect($databaseConfig);
}
TestSessionState::create()->write(); // initialize the session 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, 'SilverStripe\\Control\\Email\\Mailer')) {
throw new InvalidArgumentException(sprintf(
'Class "%s" is not a valid class, or subclass of Mailer',
$mailer
@ -291,6 +345,7 @@ class TestSessionEnvironment
}
$this->saveState($state);
$this->extend('onAfterApplyState');
}
@ -302,13 +357,13 @@ class TestSessionEnvironment
*/
public function importDatabase($path, $requireDefaultRecords = false)
{
$sql = file_get_contents($path);
$sql = file_get_contents($path ?? '');
// Split into individual query commands, removing comments
$sqlCmds = array_filter(preg_split(
'/;\n/',
preg_replace(array('/^$\n/m', '/^(\/|#).*$\n/m'), '', $sql)
));
preg_replace(array('/^$\n/m', '/^(\/|#).*$\n/m'), '', $sql ?? '') ?? ''
) ?? []);
// Execute each query
foreach ($sqlCmds as $sqlCmd) {
@ -345,7 +400,7 @@ class TestSessionEnvironment
$content = json_encode($state);
}
$old = umask(0);
file_put_contents($this->getFilePath(), $content, LOCK_EX);
file_put_contents($this->getFilePath() ?? '', $content, LOCK_EX);
umask($old);
}
@ -353,8 +408,8 @@ class TestSessionEnvironment
{
if ($this->isRunningTests()) {
try {
$contents = file_get_contents($this->getFilePath());
$json = json_decode($contents);
$contents = file_get_contents($this->getFilePath() ?? '');
$json = json_decode($contents ?? '');
$this->applyState($json);
} catch (Exception $e) {
@ -372,8 +427,8 @@ class TestSessionEnvironment
{
$file = $this->getFilePath();
if (file_exists($file)) {
if (!unlink($file)) {
if (file_exists($file ?? '')) {
if (!unlink($file ?? '')) {
throw new \Exception('Unable to remove the testsession state file, please remove it manually. File '
. 'path: ' . $file);
}
@ -395,6 +450,10 @@ class TestSessionEnvironment
{
$this->extend('onBeforeEndTestSession');
// Restore assets
$this->restoreAssets();
// Reset DB
$tempDB = new TempDatabase();
if ($tempDB->isUsed()) {
$state = $this->getState();
@ -423,15 +482,15 @@ class TestSessionEnvironment
*/
public function loadFixtureIntoDb($fixtureFile)
{
$realFile = realpath(BASE_PATH.'/'.$fixtureFile);
$baseDir = realpath(Director::baseFolder());
if (!$realFile || !file_exists($realFile)) {
$realFile = realpath(BASE_PATH . '/' . $fixtureFile);
$baseDir = realpath(Director::baseFolder() ?? '');
if (!$realFile || !file_exists($realFile ?? '')) {
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");
} elseif (substr($realFile, -4) != '.yml') {
} elseif (substr($realFile ?? '', -4) != '.yml') {
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.");
}
@ -470,6 +529,85 @@ class TestSessionEnvironment
public function getState()
{
$path = Director::getAbsFile($this->getFilePath());
return (file_exists($path)) ? json_decode(file_get_contents($path)) : new stdClass;
if (file_exists($path ?? '')) {
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

@ -39,12 +39,14 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware
// Load test state
$this->loadTestState($request);
TestSessionState::incrementState();
// Call with safe teardown
try {
return $delegate($request);
} finally {
$this->restoreTestState($request);
TestSessionState::decrementState();
}
}
@ -67,8 +69,12 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware
$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
@ -76,12 +82,7 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware
// '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
$databaseConfig = DB::getConfig();
if ($databaseConfig) {
DB::connect($databaseConfig);
}
if (!Director::isLive() && $file && file_exists($file ?? '')) {
include_once($file);
}
}

70
src/TestSessionState.php Normal file
View File

@ -0,0 +1,70 @@
<?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

@ -39,8 +39,8 @@ class TestSessionStubCodeWriter
$header = '';
// Create file incl. header if it doesn't exist
if (!file_exists($this->getFilePath())) {
touch($this->getFilePath());
if (!file_exists($this->getFilePath() ?? '')) {
touch($this->getFilePath() ?? '');
if ($this->debug) {
$header .= "<?php\n// Generated by " . $trace[1]['class'] . " on " . date('Y-m-d H:i:s') . "\n\n";
} else {
@ -52,13 +52,13 @@ class TestSessionStubCodeWriter
if ($this->debug) {
$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()
{
if (file_exists($this->getFilePath())) {
unlink($this->getFilePath());
if (file_exists($this->getFilePath() ?? '')) {
unlink($this->getFilePath() ?? '');
}
}

View File

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

View File

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

View File

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