<?php

namespace Drupal\basic_auth\Tests;

/**
 * Class BasicAuthTest.
 */
class BasicAuthTest extends \DrupalWebTestCase {

  // Data for which to create basic auth configuration, and test access.
  const DATA = [
    [
      'path' => 'admin/people/people',
      'username' => 'test',
      'password' => '1234',
    ],
    [
      'path' => 'admin/appearance',
      'username' => 'admin',
      'password' => 'passw0rd',
    ],
    [
      'path' => 'basic-auth-test',
      'username' => 'views-page',
      'password' => 'passw0rd',
      'result_after_auth' => 200,
    ],
  ];

  // Data for which to test access without creating configuration first.
  const EXTRA_DATA = [
    [
      'path' => 'admin/people/people/foo',
      'username' => 'test',
      'password' => '1234',
    ],
  ];

  /**
   * {@inheritdoc}
   */
  public static function getInfo() {
    return [
      'name' => 'Basic HTTP Authentication',
      'group' => 'Security',
      'description' => t('Testing functionality of Basic HTTP Authentication module.'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp('basic_auth_test');
  }

  /**
   * Tests that menu items with basic HTTP authentication can be rendered.
   *
   * admin_menu should not cause unnecessary authentication requests when
   * rendering such menu items.
   */
  public function testNavigationModules() {
    module_enable(['admin_menu']);

    // Let our tests know about new module enabled.
    $this->resetAll();

    if ($this->createBasicAuthConfigurations([
      [
        'path' => 'user/logout',
        'username' => 'test',
        'password' => 'test',
      ],
    ], [
      'access administration menu',
      'access administration pages',
    ])) {
      // We assume the rendered admin menu contains a 'logout' item.
      $this->drupalGet('<front>');
      if ($this->assertResponse(200, 'Admin menu successfully rendered an item with basic HTTP authentication.')) {
        $this->drupalGet('user/logout');
        $this->assertResponse(401, 'Page requires passing basic HTTP authorisation first.');
      }
    }
  }

  /**
   * Tests configuration through UI and some extra access.
   *
   * Extra access checks, which are only tested here (not using testApi(),
   * because that would be duplicate):
   * - protected-ness of a menu router item's sub-paths (that do resolve to the
   *   same menu router item);
   * - non-availability of cached pages by a different auth user.
   */
  public function testConfigForm() {
    if ($this->createBasicAuthConfigurations(static::DATA, ['access content'])) {
      $this->validate(static::DATA);
      $this->validate(static::EXTRA_DATA);
    }
    // A different user should still get an authentication request for the
    // cacheable view (which, in practice, proves that it is not cached).
    $this->drupalLogout();
    $data = array_filter(static::DATA, function ($data) {
      return $data['path'] === 'basic-auth-test';
    });
    $this->validate($data);
  }

  /**
   * Test configuration through API.
   */
  public function testApi() {
    $path = 'admin/abracadabra';

    // Try to create new configuration with non-existent path.
    try {
      basic_auth_config_edit($path);
    }
    catch (\InvalidArgumentException $e) {
      $this->assertTrue(strpos($e->getMessage(), $path) !== FALSE, 'Cannot create configuration for non-existent menu path.');
    }

    // Try to create new items without username and password.
    foreach (static::DATA as $item) {
      try {
        basic_auth_config_edit($item['path']);
      }
      catch (\InvalidArgumentException $e) {
        $this->assertTrue(strpos($e->getMessage(), 'required data') !== FALSE, 'Cannot create configuration without username or password.');
      }
    }

    // Create configuration programmatically.
    foreach (static::DATA as $item) {
      // Every configuration is enabled.
      basic_auth_config_edit($item['path'], TRUE, $item['username'], $item['password']);
    }

    // Need to reset static cache because we're doing all in a single request.
    $this->resetAll();
    $this->validate(static::DATA);

    foreach (static::DATA as $item) {
      // Disable the configuration without removal.
      basic_auth_config_edit($item['path'], FALSE);
      // Check that configuration has been disabled.
      $this->assertTrue(basic_auth_config_exists($item['path'], FALSE), sprintf('Configuration for "%s" has been disabled.', $item['path']));
      // Completely remove the configuration.
      basic_auth_config_remove($item['path']);
      // Check that configuration was removed.
      $this->assertFalse(basic_auth_config_exists($item['path']), sprintf('Configuration for "%s" has been removed.', $item['path']));
    }
  }

  /**
   * Ensure that paths restricted by HTTP and original access checks.
   *
   * @param array[] $items
   *   An array of arrays with "path", "username" and "password".
   */
  protected function validate(array $items) {
    foreach ($items as $item) {
      // The Curl handle somehow remembers if it was granted/denied access
      // last time, which could skew test results. Get a new one.
      unset($this->curlHandle);
      // Visit path, for which was configured basic HTTP authentication.
      $this->drupalGet($item['path']);
      // User should be unauthorized.
      $this->assertResponse(401, sprintf('Path "%s" requires basic HTTP authentication.', $item['path']));

      // Perform request with data for authorisation.
      $this->httpauth_credentials = sprintf('%s:%s', $item['username'], $item['password']);
      unset($this->curlHandle);
      $this->drupalGet($item['path']);
      $this->httpauth_credentials = NULL;
      $expected_result_code = isset($item['result_after_auth']) ? $item['result_after_auth'] : 403;
      $this->assertResponse($expected_result_code, sprintf('User passed basic HTTP authentication for "%s" and received a HTTP %s response.', $item['path'], $expected_result_code));
    }
  }

  /**
   * Logic as a user with permissions and create basic authentication configs.
   *
   * @param array[] $items
   *   An array of arrays with "path", "username" and "password" keys.
   * @param string[] $permissions
   *   Drupal permissions to create user with.
   *
   * @return bool
   *   A state, whether the configuration has been created correctly.
   */
  protected function createBasicAuthConfigurations(array $items, array $permissions = []) {
    $permissions[] = 'configure basic auth';

    $this->drupalLogin($this->drupalCreateUser(array_unique($permissions)));
    // Visit page with configurations.
    $this->drupalGet('admin/config/system/basic-auth');

    $configs = [];

    foreach ($items as $i => $item) {
      $item = array_intersect_key($item, ['path' => 1, 'username' => 1, 'password' => 1]);
      foreach ($item as $property => $value) {
        $configs["items[$i][$property]"] = $value;
      }

      // Pres "Add new item" to load a form.
      $this->drupalPostAJAX(NULL, NULL, ['op' => t('Add new item')]);
    }

    // Save the configuration.
    $this->drupalPost(NULL, $configs, t('Save'));

    foreach ($items as $item) {
      if (!$this->assertTrue(basic_auth_config_exists($item['path'], TRUE), sprintf('Configuration for "%s" has been created.', $item['path']))) {
        return FALSE;
      }
    }

    return TRUE;
  }

}
