diff --git a/composer.json b/composer.json
index cd4d5837..13812e79 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,12 @@
"phploc/phploc": "^7.0",
"phpmd/phpmd": "^2.10"
},
+ "autoload": {
+ "psr-4": {
+ "GovCMS\\Tests\\": ["phpunit/tests"],
+ "GovCMS\\Tests\\Integration\\": ["phpunit/integration"]
+ }
+ },
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
diff --git a/phpunit/integration/BrowserKitTrait.php b/phpunit/integration/BrowserKitTrait.php
new file mode 100644
index 00000000..f9162094
--- /dev/null
+++ b/phpunit/integration/BrowserKitTrait.php
@@ -0,0 +1,158 @@
+mink->getSession($name);
+ }
+
+ /**
+ * Returns the driver instance.
+ *
+ * @return \Behat\Mink\Driver\DriverInterface
+ * The driver instance.
+ */
+ protected function getDriverInstance(): DriverInterface {
+ if (!isset($this->driver)) {
+ $client = new DrupalTestBrowser();
+ $client->followMetaRefresh();
+ $this->driver = new BrowserKitDriver($client);
+ }
+ return $this->driver;
+ }
+
+ /**
+ * Gets the current page.
+ *
+ * @return \Behat\Mink\Element\DocumentElement
+ * The current page.
+ */
+ protected function getCurrentPage(): DocumentElement {
+ return $this->getSession()->getPage();
+ }
+
+ /**
+ * Gets the content of the current page.
+ *
+ * @return string
+ * The content of the current page.
+ */
+ protected function getCurrentPageContent(): string {
+ return $this->getCurrentPage()->getContent();
+ }
+
+ /**
+ * Sets-up a Mink session.
+ */
+ protected function setupMinkSession(): void
+ {
+ if (empty($this->baseUrl)) {
+ $this->baseUrl = getenv('SIMPLETEST_BASE_URL') ?: 'http://localhost';
+ }
+
+ $driver = $this->getDriverInstance();
+ $selectors_handler = new SelectorsHandler([
+ 'hidden_field_selector' => new HiddenFieldSelector(),
+ ]);
+ $session = new Session($driver, $selectors_handler);
+ $this->mink = new Mink([
+ 'default' => $session,
+ ]);
+ $this->mink->setDefaultSessionName('default');
+ $session->start();
+
+ // Create the artifacts directory if necessary.
+ $output_dir = getenv('BROWSERTEST_OUTPUT_DIRECTORY');
+ if ($output_dir && !is_dir($output_dir)) {
+ mkdir($output_dir, 0777, true);
+ }
+
+ if ($driver instanceof BrowserKitDriver) {
+ // Inject a Guzzle middleware to generate debug output for every request
+ // performed in the test.
+
+ // Turn off curl timeout. Having a timeout is not a problem in a normal
+ // test running, but it is a problem when debugging. Also, disable SSL
+ // peer verification so that testing under HTTPS always works.
+ $handler_stack = HandlerStack::create();
+ $handler_stack->push($this->getResponseLogHandler());
+ $client = new Client(['timeout' => null, 'verify' => false, 'handler' => $handler_stack]);
+ $driver->getClient()->setClient($client);
+ }
+
+ // According to the W3C WebDriver specification a cookie can only be set if
+ // the cookie domain is equal to the domain of the active document. When the
+ // browser starts up the active document is not our domain but 'about:blank'
+ // or similar. To be able to set our User-Agent and Xdebug cookies at the
+ // start of the test we now do a request to the front page so the active
+ // document matches the domain.
+ // @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
+ // @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
+ $this->visit($this->baseUrl . '/core/misc/druplicon.png');
+
+ // Copies cookies from the current environment, for example, XDEBUG_SESSION
+ // in order to support Xdebug.
+ $cookies = $this->extractCookiesFromRequest(Request::createFromGlobals());
+ if (isset($cookies['XDEBUG_SESSION'][0])) {
+ $session->setCookie('XDEBUG_SESSION', $cookies['XDEBUG_SESSION'][0]);
+ }
+ }
+
+ /**
+ * Stops the Mink session.
+ */
+ protected function tearDownMinkSession(): void {
+ $this->getSession()->stop();
+ // Avoid leaking memory in test cases (which are retained for a long time)
+ // by removing references to all the things.
+ $this->mink = null;
+ }
+
+ /**
+ * Visits the specified URL.
+ *
+ * @param string $url
+ * The URL to visit. If the URL does not contain a scheme, the base URL will be prepended.
+ */
+ protected function visit(string $url): void {
+ if (!parse_url($url, PHP_URL_SCHEME)) {
+ $url = $this->baseUrl . $url;
+ }
+ $this->getSession()->visit($url);
+ }
+
+}
diff --git a/phpunit/integration/Canary/Security/Functional/PasswordLengthTest.php b/phpunit/integration/Canary/Security/Functional/PasswordLengthTest.php
new file mode 100644
index 00000000..4f60b2cd
--- /dev/null
+++ b/phpunit/integration/Canary/Security/Functional/PasswordLengthTest.php
@@ -0,0 +1,56 @@
+drupalCreateUser([
+ 'administer users',
+ ]);
+ $this->drupalLogin($user);
+
+ // Test user creation page for valid password length.
+ $name = $this->randomMachineName();
+ $edit = [
+ 'name' => $name,
+ 'mail' => $this->randomMachineName() . '@example.com',
+ 'pass[pass1]' => $pass = $this->randomString(13),
+ 'pass[pass2]' => $pass,
+ 'notify' => FALSE,
+ ];
+
+ $this->drupalGet('admin/people/create');
+ $this->submitForm($edit, 'Create new account');
+ $this->assertSession()->pageTextContains('The password does not satisfy the password policies.');
+ $this->assertSession()->pageTextContains('Password length must be at least 14 characters.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ #[Override]
+ protected function setUp(): void {
+ parent::setUp();
+ // Set up the test here.
+ }
+
+}
\ No newline at end of file
diff --git a/phpunit/integration/DrupalTrait.php b/phpunit/integration/DrupalTrait.php
new file mode 100644
index 00000000..18915a22
--- /dev/null
+++ b/phpunit/integration/DrupalTrait.php
@@ -0,0 +1,122 @@
+baseUrl = $base_url;
+
+ // Include the class loader.
+ $this->classLoader = require '/app/web/autoload.php';
+
+ // Parse the base URL.
+ $parsedUrl = parse_url($this->baseUrl);
+ $host = $parsedUrl['host'] . (isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '');
+ $path = isset($parsedUrl['path']) ? rtrim($parsedUrl['path'], '/') : '';
+ $port = $parsedUrl['port'] ?? 80;
+
+ // Set up server variables.
+ $server = [
+ 'HTTP_HOST' => $host,
+ 'SERVER_PORT' => $port,
+ 'REQUEST_URI' => $path . '/',
+ 'SCRIPT_FILENAME' => $path . '/index.php',
+ 'SCRIPT_NAME' => $path . '/index.php',
+ 'PHP_SELF' => $path . '/index.php',
+ ];
+
+ // Adjust server variables for HTTPS if necessary.
+ if ($parsedUrl['scheme'] === 'https') {
+ $server['HTTPS'] = 'on';
+ }
+
+ // Create the request object.
+ $request = Request::create($this->baseUrl . '/', 'GET', [], [], [], $server);
+
+ // Initialize the Drupal kernel.
+ $this->kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod', TRUE, DRUPAL_ROOT);
+
+ // The DrupalKernel only initializes the environment once which is where
+ // it sets the Drupal error handler. We can therefore only restore it
+ // once.
+ if (!static::$restoredErrorHandler) {
+ restore_error_handler();
+ restore_exception_handler();
+ static::$restoredErrorHandler = true;
+ }
+
+ // Change the working directory to Drupal root and boot the kernel.
+ chdir(DRUPAL_ROOT);
+ $this->kernel->boot();
+ $this->kernel->preHandle($request);
+ $this->container = $this->kernel->getContainer();
+ }
+
+ /**
+ * Delete test data.
+ */
+ protected function tearDownDrupal(): void {
+ // Invalidate cache.
+ \Drupal::service('cache_tags.invalidator')->resetChecksums();
+
+ // Destroy the testing kernel.
+ if (isset($this->kernel)) {
+ $this->kernel->shutdown();
+ }
+
+ \Drupal::unsetContainer();
+ $this->container = NULL;
+ }
+
+}
diff --git a/phpunit/integration/ExistingSiteBase.php b/phpunit/integration/ExistingSiteBase.php
new file mode 100644
index 00000000..bdc59fee
--- /dev/null
+++ b/phpunit/integration/ExistingSiteBase.php
@@ -0,0 +1,109 @@
+setupMinkSession();
+ $this->setupDrupal();
+
+ // Check if the Shield module is enabled and disable it if it is.
+ $shieldConfig = $this->config('shield.settings');
+ $this->wasShieldEnabled = $shieldConfig->get('shield_enable');
+
+ if ($this->wasShieldEnabled) {
+ $shieldConfig->set('shield_enable', FALSE)->save();
+ }
+ }
+
+ /**
+ * Gets the configuration object.
+ *
+ * @param string $name
+ * The name of the configuration object.
+ *
+ * @return \Drupal\Core\Config\Config
+ * The configuration object.
+ * @throws \Exception
+ */
+ protected function config(string $name): \Drupal\Core\Config\Config {
+ return $this->container->get('config.factory')->getEditable($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown(): void {
+ // Re-enable the Shield module if it was originally enabled.
+ if ($this->wasShieldEnabled) {
+ $this->config('shield.settings')->set('shield_enable', TRUE)->save();
+ }
+
+ parent::tearDown();
+
+ $this->tearDownDrupal();
+ $this->tearDownMinkSession();
+ }
+
+ /**
+ * Prepares the request for the test case.
+ *
+ * Override this method to modify the request before it is handled.
+ */
+ protected function prepareRequest() {}
+
+}
diff --git a/phpunit/phpunit.xml b/phpunit/phpunit.xml
index b1201860..47a781a0 100644
--- a/phpunit/phpunit.xml
+++ b/phpunit/phpunit.xml
@@ -37,7 +37,10 @@
/app/web/themes
- /app/tests/phpunit/integration
+ /app/tests/phpunit/integration/GovCMS
+
+
+ /app/tests/phpunit/integration/Canary
/app/tests/phpunit/tests