diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index d42326b..80943c5 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -2613,7 +2613,7 @@ function drupal_valid_test_ua($new_prefix = NULL) {
     return $test_prefix;
   }
 
-  if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
+  if ($drupal_hash_salt && isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
     list(, $prefix, $time, $salt, $hmac) = $matches;
     $check_string =  $prefix . ';' . $time . ';' . $salt;
     // We use the salt from settings.php to make the HMAC key, since
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 85dc45c..0742788 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -264,13 +264,6 @@ function install_begin_request(&$install_state) {
   if (!$install_state['interactive']) {
     drupal_override_server_variables($install_state['server']);
   }
-  // The user agent header is used to pass a database prefix in the request when
-  // running tests. However, for security reasons, it is imperative that no
-  // installation be permitted using such a prefix.
-  elseif (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) {
-    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
-    exit;
-  }
 
   // Initialize conf_path().
   // This primes the site path to be used during installation. By not requiring
@@ -280,6 +273,14 @@ function install_begin_request(&$install_state) {
 
   drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
 
+  // The user agent header is used to pass a database prefix in the request when
+  // running tests. Make sure that a valid test token is accompanied by a
+  // settings.php overriding conf_path().
+  if ($install_state['interactive'] && drupal_valid_test_ua() && !settings()->get('simpletest_conf_path')) {
+    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
+    exit;
+  }
+
   // A request object from the HTTPFoundation to tell us about the request.
   $request = Request::createFromGlobals();
 
@@ -428,7 +429,7 @@ function install_begin_request(&$install_state) {
     $task = NULL;
 
     // Do not install over a configured settings.php.
-    if (!empty($GLOBALS['databases'])) {
+    if (!empty($GLOBALS['databases']) && !drupal_valid_test_ua()) {
       throw new Exception(install_already_done_error());
     }
   }
@@ -1060,6 +1061,10 @@ function install_settings_form($form, &$form_state, &$install_state) {
 function install_settings_form_validate($form, &$form_state) {
   $driver = $form_state['values']['driver'];
   $database = $form_state['values'][$driver];
+  if ($test_prefix = drupal_valid_test_ua()) {
+    $database['prefix'] = $test_prefix;
+    $database['password'] = $GLOBALS['databases']['default']['default']['password'];
+  }
   $drivers = drupal_get_database_types();
   $reflection = new \ReflectionClass($drivers[$driver]);
   $install_namespace = $reflection->getNamespaceName();
@@ -1068,8 +1073,11 @@ function install_settings_form_validate($form, &$form_state) {
   $database['driver'] = $driver;
 
   // TODO: remove when PIFR will be updated to use 'db_prefix' instead of
-  // 'prefix' in the database settings form.
-  $database['prefix'] = $database['db_prefix'];
+  // 'prefix' in the database settings form with the non-interactive
+  // installer.
+  if (!$test_prefix) {
+    $database['prefix'] = $database['db_prefix'];
+  }
   unset($database['db_prefix']);
 
   $form_state['storage']['database'] = $database;
@@ -1129,14 +1137,34 @@ function install_settings_form_submit($form, &$form_state) {
   global $install_state;
 
   // Update global settings array and save.
-  $settings['databases'] = (object) array(
-    'value'    => array('default' => array('default' => $form_state['storage']['database'])),
-    'required' => TRUE,
-  );
-  $settings['drupal_hash_salt'] = (object) array(
-    'value'    => drupal_hash_base64(drupal_random_bytes(55)),
-    'required' => TRUE,
-  );
+  $settings = array();
+  $database = $form_state['storage']['database'];
+  if ($test_prefix = drupal_valid_test_ua()) {
+    // This is test specific code, however it is very small and contained.
+    foreach ($form_state['storage']['database'] as $k => $v) {
+      if ($k != 'password') {
+        $settings['databases']['default']['default'][$k] = (object) array(
+          'value'    => $v,
+          'required' => TRUE,
+        );
+      }
+    }
+  }
+  else {
+    // Because of the test specific code above, this assignment here must be
+    // kept simple and must not call any functions to avoid creating a tested
+    // and a non-tested code path.
+    $settings['databases']['default']['default'] = (object) array(
+      'value'    => $database,
+      'required' => TRUE,
+    );
+  }
+  if (!$test_prefix) {
+    $settings['drupal_hash_salt'] = (object) array(
+      'value'    => drupal_hash_base64(drupal_random_bytes(55)),
+      'required' => TRUE,
+    );
+  }
 
   drupal_rewrite_settings($settings);
 
diff --git a/core/lib/Drupal/Core/SystemListingInfo.php b/core/lib/Drupal/Core/SystemListingInfo.php
index b2e42e1..989b0f2 100644
--- a/core/lib/Drupal/Core/SystemListingInfo.php
+++ b/core/lib/Drupal/Core/SystemListingInfo.php
@@ -28,7 +28,7 @@ protected function profiles($directory) {
     // For SimpleTest to be able to test modules packaged together with a
     // distribution we need to include the profile of the parent site (in
     // which test runs are triggered).
-    if (drupal_valid_test_ua()) {
+    if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
       $testing_profile = config('simpletest.settings')->get('parent_profile');
       if ($testing_profile && $testing_profile != $profile) {
         $searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory;
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php
index a4641f6..7631016 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php
@@ -64,12 +64,9 @@ function testInternalBrowser() {
       )));
       $this->assertNoTitle('Foo');
 
-      // Make sure that we are locked out of the installer when prefixing
-      // using the user-agent header. This is an important security check.
       global $base_url;
-
-      $this->drupalGet($base_url . '/core/install.php', array('external' => TRUE));
-      $this->assertResponse(403, 'Cannot access install.php with a "simpletest" user-agent header.');
+      $this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE)));
+      $this->assertResponse(403, 'Cannot access install.php.');
 
       $user = $this->drupalCreateUser();
       $this->drupalLogin($user);
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 7b50b10..4ccab14 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1354,9 +1354,9 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
         $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
       }
       if (!$ajax && isset($submit)) {
-        $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
+        $this->assertTrue($submit_matches, format_string('Found the @submit button', array('@submit' => $submit)));
       }
-      $this->fail(t('Found the requested form fields at @path', array('@path' => $path)));
+      $this->fail(format_string('Found the requested form fields at @path', array('@path' => $path)));
     }
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
new file mode 100644
index 0000000..2b8259e
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\InstallerTest.
+ */
+
+namespace Drupal\system\Tests;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\simpletest\WebTestBase;
+
+class InstallerTest extends WebTestBase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Installer tests',
+      'description' => 'Tests the interactive installer.',
+      'group' => 'Installer',
+    );
+  }
+
+  protected function setUp() {
+    global $conf;
+
+    // When running tests through the Simpletest UI (vs. on the command line),
+    // Simpletest's batch conflicts with the installer's batch. Batch API does
+    // not support the concept of nested batches (in which the nested is not
+    // progressive), so we need to temporarily pretend there was no batch.
+    // Backup the currently running Simpletest batch.
+    $this->originalBatch = batch_get();
+
+    // Create the database prefix for this test.
+    $this->prepareDatabasePrefix();
+
+    // Prepare the environment for running tests.
+    $this->prepareEnvironment();
+    if (!$this->setupEnvironment) {
+      return FALSE;
+    }
+
+    // Reset all statics and variables to perform tests in a clean environment.
+    $conf = array();
+    drupal_static_reset();
+
+    // Change the database prefix.
+    // All static variables need to be reset before the database prefix is
+    // changed, since Drupal\Core\Utility\CacheArray implementations attempt to
+    // write back to persistent caches when they are destructed.
+    $this->changeDatabasePrefix();
+    if (!$this->setupDatabasePrefix) {
+      return FALSE;
+    }
+    $variable_groups = array(
+      'system.file' => array(
+        'path.private' =>  $this->private_files_directory,
+        'path.temporary' =>  $this->temp_files_directory,
+      ),
+      'locale.settings' =>  array(
+        'translation.path' => $this->translation_files_directory,
+      ),
+    );
+    foreach ($variable_groups as $config_base => $variables) {
+      foreach ($variables as $name => $value) {
+        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
+      }
+    }
+    $GLOBALS['conf']['file_public_path'] = $this->public_files_directory;
+    $settings['conf_path'] = (object) array(
+      'value' => $this->public_files_directory,
+      'required' => TRUE,
+    );
+    $settings['config_directories'] = (object) array(
+      'value' => array(),
+      'required' => TRUE,
+    );
+    $this->writeSettings($settings);
+
+    $this->drupalGet('core/install.php?langcode=en&profile=minimal');
+    $this->drupalPost(NULL, array(), 'Save and continue');
+    // Reload config directories.
+    include $this->public_files_directory . '/settings.php';
+    $prefix = substr($this->public_files_directory, strlen(conf_path() . '/files/'));
+    foreach ($config_directories as $type => $data) {
+      $GLOBALS['config_directories'][$type]['path'] = $prefix . '/files/' . $data['path'];
+    }
+    $this->rebuildContainer();
+
+    foreach ($variable_groups as $config_base => $variables) {
+      $config = config($config_base);
+      foreach ($variables as $name => $value) {
+        $config->set($name, $value);
+      }
+      $config->save();
+    }
+    variable_set('file_public_path', $this->public_files_directory);
+
+    // Use the test mail class instead of the default mail handler class.
+    config('system.mail')->set('interface.default', 'Drupal\Core\Mail\VariableLog')->save();
+
+    drupal_set_time_limit($this->timeLimit);
+    // Temporary fix so that when running from run-tests.sh we don't get an
+    // empty current path which would indicate we're on the home page.
+    $path = current_path();
+    if (empty($path)) {
+      _current_path('run-tests');
+    }
+    $this->setup = TRUE;
+  }
+
+  protected function refreshVariables() {
+    if (!empty($this->setup)) {
+      parent::refreshVariables();
+    }
+  }
+
+  /**
+   * Overrides \Drupal\simpletest\WebTestBase::drupalGet().
+   *
+   * This override is necessary because the parent drupalGet() calls t() which
+   * is not working in early install.
+   */
+  protected function drupalGet($path, array $options = array(), array $headers = array()) {
+
+    // We re-using a CURL connection here. If that connection still has certain
+    // options set, it might change the GET into a POST. Make sure we clear out
+    // previous options.
+    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $this->getAbsoluteUrl($path), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
+    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
+
+    // Replace original page output with new output from redirected page(s).
+    if ($new = $this->checkForMetaRefresh()) {
+      $out = $new;
+    }
+    $this->verbose('GET request to: ' . $path .
+                   '<hr />Ending URL: ' . $this->getUrl() .
+                   '<hr />' . $out);
+    return $out;
+  }
+
+  function testInstaller() {
+    $this->drupalGet('user');
+  }
+
+}
