You are here

KitchenTest.php in Bakery Single Sign-On System 8.2

File

tests/src/Unit/KitchenTest.php
View source
<?php

namespace Drupal\Tests\bakery\Unit;

use Drupal\bakery\Cookies\ChocolateChip;
use Drupal\bakery\Cookies\Gingerbread;
use Drupal\bakery\Cookies\Stroopwafel;
use Drupal\bakery\Exception\MissingKeyException;
use Drupal\bakery\Kitchen;
use Drupal\Component\Datetime\Time;
use Drupal\Component\DependencyInjection\Container;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\MemoryStorage;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Http\RequestStack;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountProxy;
use Drupal\Core\Session\UserSession;
use Drupal\Tests\UnitTestCase;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request as GuzzleRequest;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use Psr\Log\Test\TestLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;

/**
 * @coversDefaultClass \Drupal\bakery\Kitchen
 */
class KitchenTest extends UnitTestCase {

  /**
   * @var \Drupal\Core\Http\RequestStack
   */
  protected $request;

  /**
   * @var \GuzzleHttp\Handler\MockHandler
   */
  protected $mockHandler;

  /**
   * @var \Drupal\Core\Session\AccountProxy
   */
  protected $currentUser;

  /**
   * @var \Symfony\Component\HttpFoundation\Session\Flash\FlashBag
   */
  protected $messageBag;

  /**
   * @var array
   */
  protected $requestHistory;

  /**
   * @var \Psr\Log\Test\TestLogger
   */
  protected $testLogger;

  /**
   * @var \Drupal\bakery\Kitchen
   */
  private $kitchen;

  /**
   * @var \Drupal\Component\Datetime\Time
   */
  private $time;

  /**
   * @var \Drupal\Core\Config\Config
   */
  private $config;

  /**
   * @var \Symfony\Component\HttpFoundation\ParameterBag
   */
  private $cookieJar;
  public function setUp() {
    parent::setUp();
    $this->request = new RequestStack();
    $this->request
      ->push(new Request());
    $this->time = new Time($this->request);
    $eventDispatcher = new EventDispatcher();
    $this->config = new Config('config.test', new MemoryStorage(), $eventDispatcher, $this
      ->prophesize(TypedConfigManagerInterface::class)
      ->reveal());
    $this->config
      ->set('bakery_key', 'test_key');
    $this->config
      ->set('bakery_cookie_extension', '_ext');
    $this->config
      ->set('bakery_domain', '.example.com');
    $this->config
      ->set('bakery_freshness', 3600);
    $this->currentUser = new AccountProxy($eventDispatcher);
    $this->cookieJar = new ParameterBag();
    $cf = $this
      ->prophesize(ConfigFactoryInterface::class);
    $cf
      ->get('bakery.settings')
      ->willReturn($this->config);
    $this->kitchen = new Kitchen($this->time, $cf
      ->reveal(), $this->currentUser, $this->cookieJar);
    $container = new Container();
    $container
      ->set('bakery.kitchen', $this->kitchen);
    \Drupal::setContainer($container);
  }
  protected function setupTestClient() {
    $this->requestHistory = [];
    $this->mockHandler = new MockHandler();
    $container = \Drupal::getContainer();
    $handlerStack = HandlerStack::create($this->mockHandler);
    $handlerStack
      ->push(Middleware::history($this->requestHistory));
    $client = new Client([
      'handler' => $handlerStack,
    ]);
    $container
      ->set('http_client', $client);
    $this->messageBag = new FlashBag();
    $container
      ->set('messenger', new Messenger($this->messageBag, new KillSwitch()));
    $container
      ->set('string_translation', $this
      ->getStringTranslationStub());

    // TODO setup a logger to capture messages for assertions.
    $this->testLogger = new TestLogger();
    $loggerFactory = new LoggerChannelFactory();
    $loggerFactory
      ->addLogger($this->testLogger);
    $container
      ->set('logger.factory', $loggerFactory);
  }

  /**
   * @covers ::bake
   * @covers ::taste
   */
  public function testBake() {
    $cookie_name = $this->kitchen
      ->cookieName(Kitchen::CHOCOLATE_CHIP);
    $cookie = new ChocolateChip('name', 'mail', 'init', FALSE);
    $this->kitchen
      ->bake($cookie);

    // Cookie was set
    $this
      ->assertTrue($this->cookieJar
      ->has($cookie_name));

    // Encryption matches bake data. Sorta niave way of asserting encryption.
    $this
      ->assertEquals($this->kitchen
      ->bakeData($cookie), $this->cookieJar
      ->get($cookie_name));

    // Tasty?
    $this
      ->assertEquals($cookie
      ->toData() + [
      'type' => $cookie_name,
      'timestamp' => $this->time
        ->getRequestTime(),
    ], $this->kitchen
      ->taste(Kitchen::CHOCOLATE_CHIP, $this->cookieJar));
  }

  /**
   * The bake test, tests success so test some failures.
   *
   * @covers ::taste
   */
  public function testTaste() {
    $this
      ->assertFalse($this->kitchen
      ->taste(Kitchen::CHOCOLATE_CHIP, $this->cookieJar), 'Missing cookie');
    $cookie = new ChocolateChip('name', 'mail', 'init', FALSE);
    $this->kitchen
      ->bake($cookie);
    $this->config
      ->set('bakery_domain', '');
    $this
      ->assertFalse($this->kitchen
      ->taste(Kitchen::CHOCOLATE_CHIP, $this->cookieJar), 'Missing cookie domain');
  }

  /**
   * @covers ::eat
   */
  public function testEat() {
    $cookie_name = $this->kitchen
      ->cookieName(Kitchen::CHOCOLATE_CHIP);
    $cookie = new ChocolateChip('name', 'mail', 'init', FALSE);
    $this->kitchen
      ->bake($cookie);
    $this
      ->assertTrue($this->cookieJar
      ->has($cookie_name));
    $this->kitchen
      ->eat(Kitchen::CHOCOLATE_CHIP);
    $this
      ->assertTrue($this->cookieJar
      ->has($cookie_name));
    $this
      ->assertEquals('', $this->cookieJar
      ->get($cookie_name));
  }

  /**
   * @covers ::cookieName
   */
  public function testCookieName() {
    $this->config
      ->set('bakery_cookie_extension', '');
    $this
      ->assertEquals('CHOCOLATECHIPSSL', $this->kitchen
      ->cookieName('CHOCOLATECHIP'));
    $this->config
      ->set('bakery_cookie_extension', 'test');
    $this
      ->assertEquals('CHOCOLATECHIPSSLtest', $this->kitchen
      ->cookieName('CHOCOLATECHIP'));
  }

  /**
   * @covers ::encrypt
   */
  public function testEncrypt() {
    $this
      ->assertEquals('Atolz3jC24C+eJehiE71pW6Rolns8EOP6Z7bHYvKjX8=', base64_encode($this->kitchen
      ->encrypt(serialize('test message'))));
  }

  /**
   * @covers ::decrypt
   */
  public function testDecrypt() {
    $this
      ->assertEquals('test message', unserialize($this->kitchen
      ->decrypt(base64_decode('Atolz3jC24C+eJehiE71pW6Rolns8EOP6Z7bHYvKjX8='))));
  }

  /**
   * @covers ::bakeData
   */
  public function testBakeDataMissingKey() {
    $this->config
      ->set('bakery_key', '');
    $this
      ->expectException(MissingKeyException::class);
    $this->kitchen
      ->bakeData(new Stroopwafel(123, 'asdf'));
  }

  /**
   * @covers ::bakeData
   * @covers ::tasteData
   */
  public function testBakeData() {
    $encoded = 'Mzc0M2RiNTMwMzJkOTE0MDFhMDMxMmVmNjQ2NmYxZGRlODMwZmM1ZjQ3ZDhkNzY5YjAwYTI1YWI3YjFlNGI1ZDYKk5BY7/qPHxsfaqbNRmpYnv5Zt5PdEKDTAaoTps/fYUAfHLHpidALZ3OxG2Nm6tFDXNmMR/SwnZMjICrc0UJgbcqe+VK4sCkHY41UDYhzHKRBHqg3S1hwVQ2727jLoEKHe5bKRpDnXA3DbIAp0p+y+qT7MvxMKPThOWEiZlpc8U3QhthD17aIXyEWgR487C5o4obdDr/w8vYTM+qyQhNMynDGdBhBTIh1mN4gd3vr';
    $cookie = new Gingerbread('tester', 0, 'child.example.com', 123);
    $timestamp = 1634021402;
    $this->request
      ->getCurrentRequest()->server
      ->set('REQUEST_TIME', $timestamp);
    $this
      ->assertEquals($encoded, $this->kitchen
      ->bakeData($cookie));
    $data = $this->kitchen
      ->tasteData($encoded, Gingerbread::getName());
    $this
      ->assertEqualsCanonicalizing([
      'name' => $cookie
        ->getAccountName(),
      'or_email' => $cookie
        ->getOrEmail(),
      'slave' => $cookie
        ->getChild(),
      'uid' => $cookie
        ->getChildUid(),
      'type' => Gingerbread::getName(),
      'timestamp' => $timestamp,
    ], $data);

    // Bad signature fails.
    $this
      ->assertFalse($this->kitchen
      ->tasteData('a' . $encoded, Gingerbread::getName()));

    // Bad cookie name fails.
    $this
      ->assertFalse($this->kitchen
      ->tasteData($encoded, Stroopwafel::getName()));

    // Stale cookies do not taste good.
    $this->request
      ->getCurrentRequest()->server
      ->set('REQUEST_TIME', $timestamp + $this->config
      ->get('bakery_freshness') + 50);
    $this
      ->assertFalse($this->kitchen
      ->tasteData($encoded, Gingerbread::getName()));
  }

  /**
   * @covers ::tasteData
   */
  public function testTasteDataMissingKey() {
    $this->config
      ->set('bakery_key', '');
    $this
      ->expectException(MissingKeyException::class);
    $this->kitchen
      ->tasteData('asdf', Stroopwafel::getName());
  }

  /**
   * @covers ::ship
   * @covers ::shipItGood
   */
  public function testShipMaster() {
    $this
      ->setupTestClient();
    $this->config
      ->set('bakery_is_master', 1);
    $this->config
      ->set('bakery_slaves', [
      'https://child0.example.com/',
      'https://child1.example.com/',
      'https://child2.example.com/',
    ]);
    $this->currentUser
      ->setAccount(new UserSession([
      'uid' => 1,
    ]));
    $cookie = new Stroopwafel(123, 'asdf');
    $this->mockHandler
      ->append(new GuzzleResponse(200, [], 'Response 1'));
    $this->mockHandler
      ->append(new RequestException(401, new GuzzleRequest('POST', 'https://example.com'), new GuzzleResponse(401, [], 'Conflict')));
    $this->mockHandler
      ->append(new BadResponseException(401, new GuzzleRequest('POST', 'https://example.com'), new GuzzleResponse(401, [], 'Conflict')));
    $this
      ->assertTrue($this->kitchen
      ->ship($cookie), 'Ship succeeded.');
    $this
      ->assertCount(3, $this->requestHistory);

    /** @var \GuzzleHttp\Psr7\Request $request */
    foreach ($this->requestHistory as $i => $history) {
      $request = $history['request'];
      $this
        ->assertEquals('https://child' . $i . '.example.com/' . $cookie
        ->getPath(), (string) $request
        ->getUri());
      $this
        ->assertEquals(http_build_query([
        'stroopwafel' => $this->kitchen
          ->bakeData($cookie),
      ]), (string) $request
        ->getBody());
    }
    $this
      ->assertEquals([
      // Status just has the body of the successful response.
      'status' => [
        'Response 1',
      ],
      // Errors have a generic message about which site failed.
      'error' => [
        Markup::create('Error occurred talking to the site at <em class="placeholder">https://child1.example.com/</em>'),
        Markup::create('Error occurred talking to the site at <em class="placeholder">https://child2.example.com/</em>'),
      ],
    ], $this->messageBag
      ->all());
    $this
      ->assertTrue($this->testLogger
      ->hasRecordThatContains('Failed to fetch file due to error', RfcLogLevel::ERROR), 'Request Error');
    $this
      ->assertTrue($this->testLogger
      ->hasRecordThatContains('Failed to fetch file due to HTTP error', RfcLogLevel::ERROR), 'HTTP Error');
  }

  /**
   * @covers ::ship
   * @covers ::shipItGood
   */
  public function testShipChild() {
    $this
      ->setupTestClient();
    $this->config
      ->set('bakery_is_master', 0);
    $this->config
      ->set('bakery_master', 'https://www.example.com/');
    $cookie = new Stroopwafel(123, 'asdf');
    $response = new GuzzleResponse();
    $this->mockHandler
      ->append($response);
    $this
      ->assertEquals($response, $this->kitchen
      ->ship($cookie));
    $this
      ->assertCount(1, $this->requestHistory);

    /** @var \GuzzleHttp\Psr7\Request $request */
    $request = $this->requestHistory[0]['request'];
    $this
      ->assertEquals('https://www.example.com/' . $cookie
      ->getPath(), (string) $request
      ->getUri());
    $this
      ->assertEquals(http_build_query([
      'stroopwafel' => $this->kitchen
        ->bakeData($cookie),
    ]), (string) $request
      ->getBody());
  }

}

Classes

Namesort descending Description
KitchenTest @coversDefaultClass \Drupal\bakery\Kitchen