first commit

This commit is contained in:
2018-04-23 21:08:22 +02:00
commit c68c1dd9b0
12994 changed files with 1500824 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\Component\Annotation\Plugin\Discovery;
use Drupal\Component\Annotation\Plugin;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
use Drupal\Component\Plugin\Definition\PluginDefinition;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator
* @group Plugin
*/
class AnnotationBridgeDecoratorTest extends UnitTestCase {
/**
* @covers ::getDefinitions
*/
public function testGetDefinitions() {
$definitions = [];
$definitions['object'] = new ObjectDefinition(['id' => 'foo']);
$definitions['array'] = ['id' => 'bar'];
$discovery = $this->prophesize(DiscoveryInterface::class);
$discovery->getDefinitions()->willReturn($definitions);
$decorator = new AnnotationBridgeDecorator($discovery->reveal(), TestAnnotation::class);
$expected = [
'object' => new ObjectDefinition(['id' => 'foo']),
'array' => new ObjectDefinition(['id' => 'bar']),
];
$this->assertEquals($expected, $decorator->getDefinitions());
}
}
class TestAnnotation extends Plugin {
/**
* {@inheritdoc}
*/
public function get() {
return new ObjectDefinition($this->definition);
}
}
class ObjectDefinition extends PluginDefinition {
/**
* ObjectDefinition constructor.
*
* @param array $definition
*/
public function __construct(array $definition) {
foreach ($definition as $property => $value) {
$this->{$property} = $value;
}
}
}
@@ -0,0 +1,266 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Assertion\InspectorTest.
*/
namespace Drupal\Tests\Component\Assertion;
use PHPUnit_Framework_TestCase;
use Drupal\Component\Assertion\Inspector;
/**
* @coversDefaultClass \Drupal\Component\Assertion\Inspector
* @group Assertion
*/
class InspectorTest extends PHPUnit_Framework_TestCase {
/**
* Tests asserting argument is an array or traversable object.
*
* @covers ::assertTraversable
*/
public function testAssertTraversable() {
$this->assertTrue(Inspector::assertTraversable([]));
$this->assertTrue(Inspector::assertTraversable(new \ArrayObject()));
$this->assertFalse(Inspector::assertTraversable(new \stdClass()));
$this->assertFalse(Inspector::assertTraversable('foo'));
}
/**
* Tests asserting all members are strings.
*
* @covers ::assertAllStrings
* @dataProvider providerTestAssertAllStrings
*/
public function testAssertAllStrings($input, $expected) {
$this->assertSame($expected, Inspector::assertAllStrings($input));
}
public function providerTestAssertAllStrings() {
$data = [
'empty-array' => [[], TRUE],
'array-with-strings' => [['foo', 'bar'], TRUE],
'string' => ['foo', FALSE],
'array-with-strings-with-colon' => [['foo', 'bar', 'llama:2001988', 'baz', 'llama:14031991'], TRUE],
'with-FALSE' => [[FALSE], FALSE],
'with-TRUE' => [[TRUE], FALSE],
'with-string-and-boolean' => [['foo', FALSE], FALSE],
'with-NULL' => [[NULL], FALSE],
'string-with-NULL' => [['foo', NULL], FALSE],
'integer' => [[1337], FALSE],
'string-and-integer' => [['foo', 1337], FALSE],
'double' => [[3.14], FALSE],
'string-and-double' => [['foo', 3.14], FALSE],
'array' => [[[]], FALSE],
'string-and-array' => [['foo', []], FALSE],
'string-and-nested-array' => [['foo', ['bar']], FALSE],
'object' => [[new \stdClass()], FALSE],
'string-and-object' => [['foo', new StringObject()], FALSE],
];
return $data;
}
/**
* Tests asserting all members are strings or objects with __toString().
*
* @covers ::assertAllStringable
*/
public function testAssertAllStringable() {
$this->assertTrue(Inspector::assertAllStringable([]));
$this->assertTrue(Inspector::assertAllStringable(['foo', 'bar']));
$this->assertFalse(Inspector::assertAllStringable('foo'));
$this->assertTrue(Inspector::assertAllStringable(['foo', new StringObject()]));
}
/**
* Tests asserting all members are arrays.
*
* @covers ::assertAllArrays
*/
public function testAssertAllArrays() {
$this->assertTrue(Inspector::assertAllArrays([]));
$this->assertTrue(Inspector::assertAllArrays([[], []]));
$this->assertFalse(Inspector::assertAllArrays([[], 'foo']));
}
/**
* Tests asserting array is 0-indexed - the strict definition of array.
*
* @covers ::assertStrictArray
*/
public function testAssertStrictArray() {
$this->assertTrue(Inspector::assertStrictArray([]));
$this->assertTrue(Inspector::assertStrictArray(['bar', 'foo']));
$this->assertFalse(Inspector::assertStrictArray(['foo' => 'bar', 'bar' => 'foo']));
}
/**
* Tests asserting all members are strict arrays.
*
* @covers ::assertAllStrictArrays
*/
public function testAssertAllStrictArrays() {
$this->assertTrue(Inspector::assertAllStrictArrays([]));
$this->assertTrue(Inspector::assertAllStrictArrays([[], []]));
$this->assertFalse(Inspector::assertAllStrictArrays([['foo' => 'bar', 'bar' => 'foo']]));
}
/**
* Tests asserting all members have specified keys.
*
* @covers ::assertAllHaveKey
*/
public function testAssertAllHaveKey() {
$this->assertTrue(Inspector::assertAllHaveKey([]));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']]));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'foo'));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo'));
$this->assertFalse(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo', 'moo'));
}
/**
* Tests asserting all members are integers.
*
* @covers ::assertAllIntegers
*/
public function testAssertAllIntegers() {
$this->assertTrue(Inspector::assertAllIntegers([]));
$this->assertTrue(Inspector::assertAllIntegers([1, 2, 3]));
$this->assertFalse(Inspector::assertAllIntegers([1, 2, 3.14]));
$this->assertFalse(Inspector::assertAllIntegers([1, '2', 3]));
}
/**
* Tests asserting all members are floating point variables.
*
* @covers ::assertAllFloat
*/
public function testAssertAllFloat() {
$this->assertTrue(Inspector::assertAllFloat([]));
$this->assertTrue(Inspector::assertAllFloat([1.0, 2.1, 3.14]));
$this->assertFalse(Inspector::assertAllFloat([1, 2.1, 3.14]));
$this->assertFalse(Inspector::assertAllFloat([1.0, '2', 3]));
$this->assertFalse(Inspector::assertAllFloat(['Titanic']));
}
/**
* Tests asserting all members are callable.
*
* @covers ::assertAllCallable
*/
public function testAllCallable() {
$this->assertTrue(Inspector::assertAllCallable([
'strchr',
[$this, 'callMe'],
[__CLASS__, 'callMeStatic'],
function() {
return TRUE;
}
]));
$this->assertFalse(Inspector::assertAllCallable([
'strchr',
[$this, 'callMe'],
[__CLASS__, 'callMeStatic'],
function() {
return TRUE;
},
"I'm not callable"
]));
}
/**
* Tests asserting all members are !empty().
*
* @covers ::assertAllNotEmpty
*/
public function testAllNotEmpty() {
$this->assertTrue(Inspector::assertAllNotEmpty([1, 'two']));
$this->assertFalse(Inspector::assertAllNotEmpty(['']));
}
/**
* Tests asserting all arguments are numbers or strings castable to numbers.
*
* @covers ::assertAllNumeric
*/
public function testAssertAllNumeric() {
$this->assertTrue(Inspector::assertAllNumeric([1, '2', 3.14]));
$this->assertFalse(Inspector::assertAllNumeric([1, 'two', 3.14]));
}
/**
* Tests asserting strstr() or stristr() match.
*
* @covers ::assertAllMatch
*/
public function testAssertAllMatch() {
$this->assertTrue(Inspector::assertAllMatch('f', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllMatch('F', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllMatch('f', ['fee', 'fi', 'fo'], TRUE));
$this->assertFalse(Inspector::assertAllMatch('F', ['fee', 'fi', 'fo'], TRUE));
$this->assertFalse(Inspector::assertAllMatch('e', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllMatch('1', [12]));
}
/**
* Tests asserting regular expression match.
*
* @covers ::assertAllRegularExpressionMatch
*/
public function testAssertAllRegularExpressionMatch() {
$this->assertTrue(Inspector::assertAllRegularExpressionMatch('/f/i', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllRegularExpressionMatch('/F/i', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllRegularExpressionMatch('/f/', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllRegularExpressionMatch('/F/', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllRegularExpressionMatch('/e/', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllRegularExpressionMatch('/1/', [12]));
}
/**
* Tests asserting all members are objects.
*
* @covers ::assertAllObjects
*/
public function testAssertAllObjects() {
$this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject()]));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject(), 'foo']));
$this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject()], '\\Traversable'));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject(), 'foo'], '\\Traversable'));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new StringObject()], '\\Traversable'));
$this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new StringObject()], '\\Traversable', '\\Drupal\\Tests\\Component\\Assertion\\StringObject'));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new StringObject(), new \stdClass()], '\\ArrayObject', '\\Drupal\\Tests\\Component\\Assertion\\StringObject'));
}
/**
* Test method referenced by ::testAllCallable().
*/
public function callMe() {
return TRUE;
}
/**
* Test method referenced by ::testAllCallable().
*/
public static function callMeStatic() {
return TRUE;
}
}
/**
* Quick class for testing for objects with __toString.
*/
class StringObject {
/**
* {@inheritdoc}
*/
public function __toString() {
return 'foo';
}
}
@@ -0,0 +1,120 @@
<?php
namespace Drupal\Tests\Component\Bridge;
use Drupal\Component\Bridge\ZfExtensionManagerSfContainer;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @coversDefaultClass \Drupal\Component\Bridge\ZfExtensionManagerSfContainer
* @group Bridge
*/
class ZfExtensionManagerSfContainerTest extends UnitTestCase {
/**
* @covers ::setContainer
* @covers ::get
*/
public function testGet() {
$service = new \stdClass();
$service->value = 'myvalue';
$container = new ContainerBuilder();
$container->set('foo', $service);
$bridge = new ZfExtensionManagerSfContainer();
$bridge->setContainer($container);
$this->assertEquals($service, $bridge->get('foo'));
}
/**
* @covers ::setContainer
* @covers ::has
*/
public function testHas() {
$service = new \stdClass();
$service->value = 'myvalue';
$container = new ContainerBuilder();
$container->set('foo', $service);
$bridge = new ZfExtensionManagerSfContainer();
$bridge->setContainer($container);
$this->assertTrue($bridge->has('foo'));
$this->assertFalse($bridge->has('bar'));
}
/**
* @covers ::__construct
* @covers ::has
* @covers ::get
*/
public function testPrefix() {
$service = new \stdClass();
$service->value = 'myvalue';
$container = new ContainerBuilder();
$container->set('foo.bar', $service);
$bridge = new ZfExtensionManagerSfContainer('foo.');
$bridge->setContainer($container);
$this->assertTrue($bridge->has('bar'));
$this->assertFalse($bridge->has('baz'));
$this->assertEquals($service, $bridge->get('bar'));
}
/**
* @covers ::canonicalizeName
* @dataProvider canonicalizeNameProvider
*/
public function testCanonicalizeName($name, $canonical_name) {
$service = new \stdClass();
$service->value = 'myvalue';
$container = new ContainerBuilder();
$container->set($canonical_name, $service);
$bridge = new ZfExtensionManagerSfContainer();
$bridge->setContainer($container);
$this->assertTrue($bridge->has($name));
$this->assertEquals($service, $bridge->get($name));
}
/**
* Data provider for testReverseProxyEnabled.
*
* Replacements:
* array('-' => '', '_' => '', ' ' => '', '\\' => '', '/' => '')
*/
public function canonicalizeNameProvider() {
return [
[
'foobar',
'foobar',
],
[
'foo-bar',
'foobar',
],
[
'foo_bar',
'foobar',
],
[
'foo bar',
'foobar',
],
[
'foo\\bar',
'foobar',
],
[
'foo/bar',
'foobar',
],
// There is also a strtolower in canonicalizeName.
[
'Foo/bAr',
'foobar',
],
[
'foo/-_\\ bar',
'foobar',
],
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace Drupal\Tests\Component\ClassFinder;
use Composer\Autoload\ClassLoader;
use Drupal\Component\ClassFinder\ClassFinder;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\ClassFinder\ClassFinder
* @group ClassFinder
*/
class ClassFinderTest extends UnitTestCase {
/**
* @covers ::findFile
*/
public function testFindFile() {
$finder = new ClassFinder();
// The full path is returned therefore only tests with
// assertStringEndsWith() so the test is portable.
$this->assertStringEndsWith('core/tests/Drupal/Tests/UnitTestCase.php', $finder->findFile(UnitTestCase::class));
$class = 'Not\\A\\Class';
$this->assertNull($finder->findFile($class));
// Register an autoloader that can find this class.
$loader = new ClassLoader();
$loader->addClassMap([$class => __FILE__]);
$loader->register();
$this->assertEquals(__FILE__, $finder->findFile($class));
// This shouldn't prevent us from finding the original file.
$this->assertStringEndsWith('core/tests/Drupal/Tests/UnitTestCase.php', $finder->findFile(UnitTestCase::class));
// Clean up the additional autoloader after the test.
$loader->unregister();
}
}
@@ -0,0 +1,781 @@
<?php
namespace Drupal\Tests\Component\Datetime;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Datetime\DateTimePlus;
/**
* @coversDefaultClass \Drupal\Component\Datetime\DateTimePlus
* @group Datetime
*/
class DateTimePlusTest extends UnitTestCase {
/**
* Test creating dates from string and array input.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $expected
* Expected output from DateTimePlus::format().
*
* @dataProvider providerTestDates
*/
public function testDates($input, $timezone, $expected) {
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
if (is_array($input)) {
$input = var_export($input, TRUE);
}
$this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
}
/**
* Test creating dates from string and array input.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $expected
* Expected output from DateTimePlus::format().
*
* @dataProvider providerTestDateArrays
*/
public function testDateArrays($input, $timezone, $expected) {
$date = DateTimePlus::createFromArray($input, $timezone);
$value = $date->format('c');
if (is_array($input)) {
$input = var_export($input, TRUE);
}
$this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
}
/**
* Test date diffs.
*
* @param mixed $input1
* A DateTimePlus object.
* @param mixed $input2
* Date argument for DateTimePlus::diff method.
* @param bool $absolute
* Absolute flag for DateTimePlus::diff method.
* @param \DateInterval $expected
* The expected result of the DateTimePlus::diff operation.
*
* @dataProvider providerTestDateDiff
*/
public function testDateDiff($input1, $input2, $absolute, \DateInterval $expected) {
$interval = $input1->diff($input2, $absolute);
$this->assertEquals($interval, $expected);
}
/**
* Test date diff exception caused by invalid input.
*
* @param mixed $input1
* A DateTimePlus object.
* @param mixed $input2
* Date argument for DateTimePlus::diff method.
* @param bool $absolute
* Absolute flag for DateTimePlus::diff method.
*
* @dataProvider providerTestInvalidDateDiff
*/
public function testInvalidDateDiff($input1, $input2, $absolute) {
$this->setExpectedException(\BadMethodCallException::class, 'Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object');
$interval = $input1->diff($input2, $absolute);
}
/**
* Test creating dates from invalid array input.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $class
* The Exception subclass to expect to be thrown.
*
* @dataProvider providerTestInvalidDateArrays
*/
public function testInvalidDateArrays($input, $timezone, $class) {
$this->setExpectedException($class);
$this->assertInstanceOf(
'\Drupal\Component\DateTimePlus',
DateTimePlus::createFromArray($input, $timezone)
);
}
/**
* Test creating dates from timestamps, and manipulating timezones.
*
* @param int $input
* Input argument for DateTimePlus::createFromTimestamp().
* @param array $initial
* An array containing:
* - 'timezone_initial' - Timezone argument for DateTimePlus.
* - 'format_initial' - Format argument for DateTimePlus.
* - 'expected_initial_date' - Expected output from DateTimePlus::format().
* - 'expected_initial_timezone' - Expected output from
* DateTimePlus::getTimeZone()::getName().
* - 'expected_initial_offset' - Expected output from DateTimePlus::getOffset().
* @param array $transform
* An array containing:
* - 'timezone_transform' - Argument to transform date to another timezone via
* DateTimePlus::setTimezone().
* - 'format_transform' - Format argument to use when transforming date to
* another timezone.
* - 'expected_transform_date' - Expected output from DateTimePlus::format(),
* after timezone transform.
* - 'expected_transform_timezone' - Expected output from
* DateTimePlus::getTimeZone()::getName(), after timezone transform.
* - 'expected_transform_offset' - Expected output from
* DateTimePlus::getOffset(), after timezone transform.
*
* @dataProvider providerTestTimestamp
*/
public function testTimestamp($input, array $initial, array $transform) {
// Initialize a new date object.
$date = DateTimePlus::createFromTimestamp($input, $initial['timezone']);
$this->assertDateTimestamp($date, $input, $initial, $transform);
}
/**
* Test creating dates from datetime strings.
*
* @param string $input
* Input argument for DateTimePlus().
* @param array $initial
* @see testTimestamp()
* @param array $transform
* @see testTimestamp()
*
* @dataProvider providerTestDateTimestamp
*/
public function testDateTimestamp($input, array $initial, array $transform) {
// Initialize a new date object.
$date = new DateTimePlus($input, $initial['timezone']);
$this->assertDateTimestamp($date, $input, $initial, $transform);
}
/**
* Assertion helper for testTimestamp and testDateTimestamp since they need
* different dataProviders.
*
* @param DateTimePlus $date
* DateTimePlus to test.
* @input mixed $input
* The original input passed to the test method.
* @param array $initial
* @see testTimestamp()
* @param array $transform
* @see testTimestamp()
*/
public function assertDateTimestamp($date, $input, $initial, $transform) {
// Check format.
$value = $date->format($initial['format']);
$this->assertEquals($initial['expected_date'], $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $initial['timezone'], $initial['expected_date'], $value));
// Check timezone name.
$value = $date->getTimeZone()->getName();
$this->assertEquals($initial['expected_timezone'], $value, sprintf("The current timezone is %s: should be %s.", $value, $initial['expected_timezone']));
// Check offset.
$value = $date->getOffset();
$this->assertEquals($initial['expected_offset'], $value, sprintf("The current offset is %s: should be %s.", $value, $initial['expected_offset']));
// Transform the date to another timezone.
$date->setTimezone(new \DateTimeZone($transform['timezone']));
// Check transformed format.
$value = $date->format($transform['format']);
$this->assertEquals($transform['expected_date'], $value, sprintf("Test \$date->setTimezone(new \\DateTimeZone(%s)): should be %s, found %s.", $transform['timezone'], $transform['expected_date'], $value));
// Check transformed timezone.
$value = $date->getTimeZone()->getName();
$this->assertEquals($transform['expected_timezone'], $value, sprintf("The current timezone should be %s, found %s.", $transform['expected_timezone'], $value));
// Check transformed offset.
$value = $date->getOffset();
$this->assertEquals($transform['expected_offset'], $value, sprintf("The current offset should be %s, found %s.", $transform['expected_offset'], $value));
}
/**
* Test creating dates from format strings.
*
* @param string $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $format_date
* Format argument for DateTimePlus::format().
* @param string $expected
* Expected output from DateTimePlus::format().
*
* @dataProvider providerTestDateFormat
*/
public function testDateFormat($input, $timezone, $format, $format_date, $expected) {
$date = DateTimePlus::createFromFormat($format, $input, $timezone);
$value = $date->format($format_date);
$this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s, %s): should be %s, found %s.", $input, $timezone, $format, $expected, $value));
}
/**
* Test invalid date handling.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $format
* Format argument for DateTimePlus.
* @param string $message
* Message to print if no errors are thrown by the invalid dates.
* @param string $class
* The Exception subclass to expect to be thrown.
*
* @dataProvider providerTestInvalidDates
*/
public function testInvalidDates($input, $timezone, $format, $message, $class) {
$this->setExpectedException($class);
DateTimePlus::createFromFormat($format, $input, $timezone);
}
/**
* Tests that DrupalDateTime can detect the right timezone to use.
* When specified or not.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param mixed $timezone
* Timezone argument for DateTimePlus.
* @param string $expected_timezone
* Expected timezone returned from DateTimePlus::getTimezone::getName().
* @param string $message
* Message to print on test failure.
*
* @dataProvider providerTestDateTimezone
*/
public function testDateTimezone($input, $timezone, $expected_timezone, $message) {
$date = new DateTimePlus($input, $timezone);
$timezone = $date->getTimezone()->getName();
$this->assertEquals($timezone, $expected_timezone, $message);
}
/**
* Test that DrupalDateTime can detect the right timezone to use when
* constructed from a datetime object.
*/
public function testDateTimezoneWithDateTimeObject() {
// Create a date object with another date object.
$input = new \DateTime('now', new \DateTimeZone('Pacific/Midway'));
$timezone = NULL;
$expected_timezone = 'Pacific/Midway';
$message = 'DateTimePlus uses the specified timezone if provided.';
$date = DateTimePlus::createFromDateTime($input, $timezone);
$timezone = $date->getTimezone()->getName();
$this->assertEquals($timezone, $expected_timezone, $message);
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testDates().
*
* @see DateTimePlusTest::testDates()
*/
public function providerTestDates() {
return [
// String input.
// Create date object from datetime string.
['2009-03-07 10:30', 'America/Chicago', '2009-03-07T10:30:00-06:00'],
// Same during daylight savings time.
['2009-06-07 10:30', 'America/Chicago', '2009-06-07T10:30:00-05:00'],
// Create date object from date string.
['2009-03-07', 'America/Chicago', '2009-03-07T00:00:00-06:00'],
// Same during daylight savings time.
['2009-06-07', 'America/Chicago', '2009-06-07T00:00:00-05:00'],
// Create date object from date string.
['2009-03-07 10:30', 'Australia/Canberra', '2009-03-07T10:30:00+11:00'],
// Same during daylight savings time.
['2009-06-07 10:30', 'Australia/Canberra', '2009-06-07T10:30:00+10:00'],
];
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testDates().
*
* @see DateTimePlusTest::testDates()
*/
public function providerTestDateArrays() {
return [
// Array input.
// Create date object from date array, date only.
[['year' => 2010, 'month' => 2, 'day' => 28], 'America/Chicago', '2010-02-28T00:00:00-06:00'],
// Create date object from date array with hour.
[['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'America/Chicago', '2010-02-28T10:00:00-06:00'],
// Create date object from date array, date only.
[['year' => 2010, 'month' => 2, 'day' => 28], 'Europe/Berlin', '2010-02-28T00:00:00+01:00'],
// Create date object from date array with hour.
[['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'Europe/Berlin', '2010-02-28T10:00:00+01:00'],
];
}
/**
* Provides data for testDateFormats.
*
* @return array
* An array of arrays, each containing:
* - 'input' - Input to DateTimePlus.
* - 'timezone' - Timezone for DateTimePlus.
* - 'format' - Date format for DateTimePlus.
* - 'format_date' - Date format for use in $date->format() method.
* - 'expected' - The expected return from DateTimePlus.
*
* @see testDateFormats()
*/
public function providerTestDateFormat() {
return [
// Create a year-only date.
['2009', NULL, 'Y', 'Y', '2009'],
// Create a month and year-only date.
['2009-10', NULL, 'Y-m', 'Y-m', '2009-10'],
// Create a time-only date.
['T10:30:00', NULL, '\TH:i:s', 'H:i:s', '10:30:00'],
// Create a time-only date.
['10:30:00', NULL, 'H:i:s', 'H:i:s', '10:30:00'],
];
}
/**
* Provides data for testInvalidDates.
*
* @return array
* An array of arrays, each containing:
* - 'input' - Input for DateTimePlus.
* - 'timezone' - Timezone for DateTimePlus.
* - 'format' - Format for DateTimePlus.
* - 'message' - Message to display on failure.
*
* @see testInvalidDates
*/
public function providerTestInvalidDates() {
return [
// Test for invalid month names when we are using a short version
// of the month.
['23 abc 2012', NULL, 'd M Y', "23 abc 2012 contains an invalid month name and did not produce errors.", \InvalidArgumentException::class],
// Test for invalid hour.
['0000-00-00T45:30:00', NULL, 'Y-m-d\TH:i:s', "0000-00-00T45:30:00 contains an invalid hour and did not produce errors.", \UnexpectedValueException::class],
// Test for invalid day.
['0000-00-99T05:30:00', NULL, 'Y-m-d\TH:i:s', "0000-00-99T05:30:00 contains an invalid day and did not produce errors.", \UnexpectedValueException::class],
// Test for invalid month.
['0000-75-00T15:30:00', NULL, 'Y-m-d\TH:i:s', "0000-75-00T15:30:00 contains an invalid month and did not produce errors.", \UnexpectedValueException::class],
// Test for invalid year.
['11-08-01T15:30:00', NULL, 'Y-m-d\TH:i:s', "11-08-01T15:30:00 contains an invalid year and did not produce errors.", \UnexpectedValueException::class],
];
}
/**
* Data provider for testInvalidDateArrays.
*
* @return array
* An array of arrays, each containing:
* - 'input' - Input for DateTimePlus.
* - 'timezone' - Timezone for DateTimePlus.
*
* @see testInvalidDateArrays
*/
public function providerTestInvalidDateArrays() {
return [
// One year larger than the documented upper limit of checkdate().
[['year' => 32768, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// One year smaller than the documented lower limit of checkdate().
[['year' => 0, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Test for invalid month from date array.
[['year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Test for invalid hour from date array.
[['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Test for invalid minute from date array.
[['year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Regression test for https://www.drupal.org/node/2084455.
[['hour' => 59, 'minute' => 1, 'second' => 1], 'America/Chicago', \InvalidArgumentException::class],
];
}
/**
* Provides data for testDateTimezone.
*
* @return array
* An array of arrays, each containing:
* - 'date' - Date string or object for DateTimePlus.
* - 'timezone' - Timezone string for DateTimePlus.
* - 'expected' - Expected return from DateTimePlus::getTimezone()::getName().
* - 'message' - Message to display on test failure.
*
* @see testDateTimezone
*/
public function providerTestDateTimezone() {
// Use a common date for most of the tests.
$date_string = '2007-01-31 21:00:00';
// Detect the system timezone.
$system_timezone = date_default_timezone_get();
return [
// Create a date object with an unspecified timezone, which should
// end up using the system timezone.
[$date_string, NULL, $system_timezone, 'DateTimePlus uses the system timezone when there is no site timezone.'],
// Create a date object with a specified timezone name.
[$date_string, 'America/Yellowknife', 'America/Yellowknife', 'DateTimePlus uses the specified timezone if provided.'],
// Create a date object with a timezone object.
[$date_string, new \DateTimeZone('Australia/Canberra'), 'Australia/Canberra', 'DateTimePlus uses the specified timezone if provided.'],
// Create a date object with another date object.
[new DateTimePlus('now', 'Pacific/Midway'), NULL, 'Pacific/Midway', 'DateTimePlus uses the specified timezone if provided.'],
];
}
/**
* Provides data for testTimestamp.
*
* @return array
* An array of arrays, each containing the arguments required for
* self::testTimestamp().
*
* @see testTimestamp()
*/
public function providerTestTimestamp() {
return [
// Create date object from a unix timestamp and display it in
// local time.
[
'input' => 0,
'initial' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
'transform' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
],
// Create a date using the timestamp of zero, then display its
// value both in UTC and the local timezone.
[
'input' => 0,
'initial' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
'transform' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
],
];
}
/**
* Provides data for testDateTimestamp.
*
* @return array
* An array of arrays, each containing the arguments required for
* self::testDateTimestamp().
*
* @see testDateTimestamp()
*/
public function providerTestDateTimestamp() {
return [
// Create date object from datetime string in UTC, and convert
// it to a local date.
[
'input' => '1970-01-01 00:00:00',
'initial' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
'transform' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
],
// Convert the local time to UTC using string input.
[
'input' => '1969-12-31 16:00:00',
'initial' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
'transform' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
],
// Convert the local time to UTC using string input.
[
'input' => '1969-12-31 16:00:00',
'initial' => [
'timezone' => 'Europe/Warsaw',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00+01:00',
'expected_timezone' => 'Europe/Warsaw',
'expected_offset' => '+3600',
],
'transform' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1969-12-31T15:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
],
];
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testDateDiff().
*
* @see DateTimePlusTest::testDateDiff()
*/
public function providerTestDateDiff() {
$empty_interval = new \DateInterval('PT0S');
$positive_19_hours = new \DateInterval('PT19H');
$positive_18_hours = new \DateInterval('PT18H');
$positive_1_hour = new \DateInterval('PT1H');
$negative_1_hour = new \DateInterval('PT1H');
$negative_1_hour->invert = 1;
return [
// There should be a 19 hour time interval between
// new years in Sydney and new years in LA in year 2000.
[
'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
'absolute' => FALSE,
'expected' => $positive_19_hours,
],
// In 1970 Sydney did not observe daylight savings time
// So there is only a 18 hour time interval.
[
'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
'absolute' => FALSE,
'expected' => $positive_18_hours,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600, new \DateTimeZone('America/Los_Angeles')),
'input2' => DateTimePlus::createFromFormat('U', 0, new \DateTimeZone('UTC')),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => DateTimePlus::createFromFormat('U', 0),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => \DateTime::createFromFormat('U', 0),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => DateTimePlus::createFromFormat('U', 0),
'absolute' => TRUE,
'expected' => $positive_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => \DateTime::createFromFormat('U', 0),
'absolute' => TRUE,
'expected' => $positive_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 0),
'input2' => DateTimePlus::createFromFormat('U', 0),
'absolute' => FALSE,
'expected' => $empty_interval,
],
];
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testInvalidDateDiff().
*
* @see DateTimePlusTest::testInvalidDateDiff()
*/
public function providerTestInvalidDateDiff() {
return [
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => '1970-01-01 00:00:00',
'absolute' => FALSE,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => NULL,
'absolute' => FALSE,
],
];
}
/**
* Tests invalid values passed to constructor.
*
* @param string $time
* A date/time string.
* @param string[] $errors
* An array of error messages.
*
* @covers ::__construct
*
* @dataProvider providerTestInvalidConstructor
*/
public function testInvalidConstructor($time, array $errors) {
$date = new DateTimePlus($time);
$this->assertEquals(TRUE, $date->hasErrors());
$this->assertEquals($errors, $date->getErrors());
}
/**
* Provider for testInvalidConstructor().
*
* @return array
* An array of invalid date/time strings, and corresponding error messages.
*/
public function providerTestInvalidConstructor() {
return [
[
'YYYY-MM-DD',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'2017-MM-DD',
[
'Unexpected character',
'The timezone could not be found in the database',
],
],
[
'YYYY-03-DD',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'YYYY-MM-07',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'2017-13-55',
[
'Unexpected character',
],
],
[
'YYYY-MM-DD hh:mm:ss',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'2017-03-07 25:70:80',
[
'Unexpected character',
'Double time specification',
],
],
[
'lorem ipsum dolor sit amet',
[
'The timezone could not be found in the database',
'Double timezone specification',
],
],
];
}
/**
* Tests the $settings['validate_format'] parameter in ::createFromFormat().
*/
public function testValidateFormat() {
// Check that an input that does not strictly follow the input format will
// produce the desired date. In this case the year string '11' doesn't
// precisely match the 'Y' formater parameter, but PHP will parse it
// regardless. However, when formatted with the same string, the year will
// be output with four digits. With the ['validate_format' => FALSE]
// $settings, this will not thrown an exception.
$date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => FALSE]);
$this->assertEquals('0011-03-31 17:44:00', $date->format('Y-m-d H:i:s'));
// Parse the same date with ['validate_format' => TRUE] and make sure we
// get the expected exception.
$this->setExpectedException(\UnexpectedValueException::class);
$date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => TRUE]);
}
}
@@ -0,0 +1,123 @@
<?php
namespace Drupal\Tests\Component\Datetime;
use Drupal\Component\Datetime\Time;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\Component\Datetime\Time
* @group Datetime
*
* Isolate the tests to prevent side effects from altering system time.
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TimeTest extends UnitTestCase {
/**
* The mocked request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack|\PHPUnit_Framework_MockObject_MockObject
*/
protected $requestStack;
/**
* The mocked time class.
*
* @var \Drupal\Component\Datetime\Time
*/
protected $time;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack');
$this->time = new Time($this->requestStack);
}
/**
* Tests the getRequestTime method.
*
* @covers ::getRequestTime
*/
public function testGetRequestTime() {
$expected = 12345678;
$request = Request::createFromGlobals();
$request->server->set('REQUEST_TIME', $expected);
// Mocks a the request stack getting the current request.
$this->requestStack->expects($this->any())
->method('getCurrentRequest')
->willReturn($request);
$this->assertEquals($expected, $this->time->getRequestTime());
}
/**
* Tests the getRequestMicroTime method.
*
* @covers ::getRequestMicroTime
*/
public function testGetRequestMicroTime() {
$expected = 1234567.89;
$request = Request::createFromGlobals();
$request->server->set('REQUEST_TIME_FLOAT', $expected);
// Mocks a the request stack getting the current request.
$this->requestStack->expects($this->any())
->method('getCurrentRequest')
->willReturn($request);
$this->assertEquals($expected, $this->time->getRequestMicroTime());
}
/**
* Tests the getCurrentTime method.
*
* @covers ::getCurrentTime
*/
public function testGetCurrentTime() {
$expected = 12345678;
$this->assertEquals($expected, $this->time->getCurrentTime());
}
/**
* Tests the getCurrentMicroTime method.
*
* @covers ::getCurrentMicroTime
*/
public function testGetCurrentMicroTime() {
$expected = 1234567.89;
$this->assertEquals($expected, $this->time->getCurrentMicroTime());
}
}
namespace Drupal\Component\Datetime;
/**
* Shadow time() system call.
*
* @returns int
*/
function time() {
return 12345678;
}
/**
* Shadow microtime system call.
*
* @returns float
*/
function microtime() {
return 1234567.89;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,682 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumperTest.
*/
namespace Drupal\Tests\Component\DependencyInjection\Dumper {
use Drupal\Component\Utility\Crypt;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper
* @group DependencyInjection
*/
class OptimizedPhpArrayDumperTest extends \PHPUnit_Framework_TestCase {
/**
* The container builder instance.
*
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $containerBuilder;
/**
* The definition for the container to build in tests.
*
* @var array
*/
protected $containerDefinition;
/**
* Whether the dumper uses the machine-optimized format or not.
*
* @var bool
*/
protected $machineFormat = TRUE;
/**
* Stores the dumper class to use.
*
* @var string
*/
protected $dumperClass = '\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper';
/**
* The dumper instance.
*
* @var \Symfony\Component\DependencyInjection\Dumper\DumperInterface
*/
protected $dumper;
/**
* {@inheritdoc}
*/
protected function setUp() {
// Setup a mock container builder.
$this->containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder');
$this->containerBuilder->getAliases()->willReturn([]);
$this->containerBuilder->getParameterBag()->willReturn(new ParameterBag());
$this->containerBuilder->getDefinitions()->willReturn(NULL);
$this->containerBuilder->isFrozen()->willReturn(TRUE);
$definition = [];
$definition['aliases'] = [];
$definition['parameters'] = [];
$definition['services'] = [];
$definition['frozen'] = TRUE;
$definition['machine_format'] = $this->machineFormat;
$this->containerDefinition = $definition;
// Create the dumper.
$this->dumper = new $this->dumperClass($this->containerBuilder->reveal());
}
/**
* Tests that an empty container works properly.
*
* @covers ::dump
* @covers ::getArray
* @covers ::supportsMachineFormat
*/
public function testDumpForEmptyContainer() {
$serialized_definition = $this->dumper->dump();
$this->assertEquals(serialize($this->containerDefinition), $serialized_definition);
}
/**
* Tests that alias processing works properly.
*
* @covers ::getAliases
*
* @dataProvider getAliasesDataProvider
*/
public function testGetAliases($aliases, $definition_aliases) {
$this->containerDefinition['aliases'] = $definition_aliases;
$this->containerBuilder->getAliases()->willReturn($aliases);
$this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.');
}
/**
* Data provider for testGetAliases().
*
* @return array[]
* Returns data-set elements with:
* - aliases as returned by ContainerBuilder.
* - aliases as expected in the container definition.
*/
public function getAliasesDataProvider() {
return [
[[], []],
[
['foo' => 'foo.alias'],
['foo' => 'foo.alias'],
],
[
['foo' => 'foo.alias', 'foo.alias' => 'foo.alias.alias'],
['foo' => 'foo.alias.alias', 'foo.alias' => 'foo.alias.alias'],
],
];
}
/**
* Tests that parameter processing works properly.
*
* @covers ::getParameters
* @covers ::prepareParameters
* @covers ::escape
* @covers ::dumpValue
* @covers ::getReferenceCall
*
* @dataProvider getParametersDataProvider
*/
public function testGetParameters($parameters, $definition_parameters, $is_frozen) {
$this->containerDefinition['parameters'] = $definition_parameters;
$this->containerDefinition['frozen'] = $is_frozen;
$parameter_bag = new ParameterBag($parameters);
$this->containerBuilder->getParameterBag()->willReturn($parameter_bag);
$this->containerBuilder->isFrozen()->willReturn($is_frozen);
if (isset($parameters['reference'])) {
$definition = new Definition('\stdClass');
$this->containerBuilder->getDefinition('referenced_service')->willReturn($definition);
}
$this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.');
}
/**
* Data provider for testGetParameters().
*
* @return array[]
* Returns data-set elements with:
* - parameters as returned by ContainerBuilder.
* - parameters as expected in the container definition.
* - frozen value
*/
public function getParametersDataProvider() {
return [
[[], [], TRUE],
[
['foo' => 'value_foo'],
['foo' => 'value_foo'],
TRUE,
],
[
['foo' => ['llama' => 'yes']],
['foo' => ['llama' => 'yes']],
TRUE,
],
[
['foo' => '%llama%', 'llama' => 'yes'],
['foo' => '%%llama%%', 'llama' => 'yes'],
TRUE,
],
[
['foo' => '%llama%', 'llama' => 'yes'],
['foo' => '%llama%', 'llama' => 'yes'],
FALSE,
],
[
['reference' => new Reference('referenced_service')],
['reference' => $this->getServiceCall('referenced_service')],
TRUE,
],
];
}
/**
* Tests that service processing works properly.
*
* @covers ::getServiceDefinitions
* @covers ::getServiceDefinition
* @covers ::dumpMethodCalls
* @covers ::dumpCollection
* @covers ::dumpCallable
* @covers ::dumpValue
* @covers ::getPrivateServiceCall
* @covers ::getReferenceCall
* @covers ::getServiceCall
* @covers ::getParameterCall
*
* @dataProvider getDefinitionsDataProvider
*/
public function testGetServiceDefinitions($services, $definition_services) {
$this->containerDefinition['services'] = $definition_services;
$this->containerBuilder->getDefinitions()->willReturn($services);
$bar_definition = new Definition('\stdClass');
$this->containerBuilder->getDefinition('bar')->willReturn($bar_definition);
$private_definition = new Definition('\stdClass');
$private_definition->setPublic(FALSE);
$this->containerBuilder->getDefinition('private_definition')->willReturn($private_definition);
$this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.');
}
/**
* Data provider for testGetServiceDefinitions().
*
* @return array[]
* Returns data-set elements with:
* - parameters as returned by ContainerBuilder.
* - parameters as expected in the container definition.
* - frozen value
*/
public function getDefinitionsDataProvider() {
$base_service_definition = [
'class' => '\stdClass',
'public' => TRUE,
'file' => FALSE,
'synthetic' => FALSE,
'lazy' => FALSE,
'arguments' => [],
'arguments_count' => 0,
'properties' => [],
'calls' => [],
'scope' => ContainerInterface::SCOPE_CONTAINER,
'shared' => TRUE,
'factory' => FALSE,
'configurator' => FALSE,
];
// Test basic flags.
$service_definitions[] = [] + $base_service_definition;
$service_definitions[] = [
'public' => FALSE,
] + $base_service_definition;
$service_definitions[] = [
'file' => 'test_include.php',
] + $base_service_definition;
$service_definitions[] = [
'synthetic' => TRUE,
] + $base_service_definition;
$service_definitions[] = [
'shared' => FALSE,
] + $base_service_definition;
$service_definitions[] = [
'lazy' => TRUE,
] + $base_service_definition;
// Test a basic public Reference.
$service_definitions[] = [
'arguments' => ['foo', new Reference('bar')],
'arguments_count' => 2,
'arguments_expected' => $this->getCollection(['foo', $this->getServiceCall('bar')]),
] + $base_service_definition;
// Test a public reference that should not throw an Exception.
$reference = new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE);
$service_definitions[] = [
'arguments' => [$reference],
'arguments_count' => 1,
'arguments_expected' => $this->getCollection([$this->getServiceCall('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE)]),
] + $base_service_definition;
// Test a private shared service, denoted by having a Reference.
$private_definition = [
'class' => '\stdClass',
'public' => FALSE,
'arguments_count' => 0,
];
$service_definitions[] = [
'arguments' => ['foo', new Reference('private_definition')],
'arguments_count' => 2,
'arguments_expected' => $this->getCollection([
'foo',
$this->getPrivateServiceCall('private_definition', $private_definition, TRUE),
]),
] + $base_service_definition;
// Test a private non-shared service, denoted by having a Definition.
$private_definition_object = new Definition('\stdClass');
$private_definition_object->setPublic(FALSE);
$service_definitions[] = [
'arguments' => ['foo', $private_definition_object],
'arguments_count' => 2,
'arguments_expected' => $this->getCollection([
'foo',
$this->getPrivateServiceCall(NULL, $private_definition),
]),
] + $base_service_definition;
// Test a deep collection without a reference.
$service_definitions[] = [
'arguments' => [[['foo']]],
'arguments_count' => 1,
] + $base_service_definition;
// Test a deep collection with a reference to resolve.
$service_definitions[] = [
'arguments' => [[new Reference('bar')]],
'arguments_count' => 1,
'arguments_expected' => $this->getCollection([$this->getCollection([$this->getServiceCall('bar')])]),
] + $base_service_definition;
// Test a collection with a variable to resolve.
$service_definitions[] = [
'arguments' => [new Parameter('llama_parameter')],
'arguments_count' => 1,
'arguments_expected' => $this->getCollection([$this->getParameterCall('llama_parameter')]),
] + $base_service_definition;
// Test objects that have _serviceId property.
$drupal_service = new \stdClass();
$drupal_service->_serviceId = 'bar';
$service_definitions[] = [
'arguments' => [$drupal_service],
'arguments_count' => 1,
'arguments_expected' => $this->getCollection([$this->getServiceCall('bar')]),
] + $base_service_definition;
// Test getMethodCalls.
$calls = [
['method', $this->getCollection([])],
['method2', $this->getCollection([])],
];
$service_definitions[] = [
'calls' => $calls,
] + $base_service_definition;
$service_definitions[] = [
'scope' => ContainerInterface::SCOPE_PROTOTYPE,
'shared' => FALSE,
] + $base_service_definition;
$service_definitions[] = [
'shared' => FALSE,
] + $base_service_definition;
// Test factory.
$service_definitions[] = [
'factory' => [new Reference('bar'), 'factoryMethod'],
'factory_expected' => [$this->getServiceCall('bar'), 'factoryMethod'],
] + $base_service_definition;
// Test invalid factory - needed to test deep dumpValue().
$service_definitions[] = [
'factory' => [['foo', 'llama'], 'factoryMethod'],
] + $base_service_definition;
// Test properties.
$service_definitions[] = [
'properties' => ['_value' => 'llama'],
] + $base_service_definition;
// Test configurator.
$service_definitions[] = [
'configurator' => [new Reference('bar'), 'configureService'],
'configurator_expected' => [$this->getServiceCall('bar'), 'configureService'],
] + $base_service_definition;
$services_provided = [];
$services_provided[] = [
[],
[],
];
foreach ($service_definitions as $service_definition) {
$definition = $this->prophesize('\Symfony\Component\DependencyInjection\Definition');
$definition->getClass()->willReturn($service_definition['class']);
$definition->isPublic()->willReturn($service_definition['public']);
$definition->getFile()->willReturn($service_definition['file']);
$definition->isSynthetic()->willReturn($service_definition['synthetic']);
$definition->isLazy()->willReturn($service_definition['lazy']);
$definition->getArguments()->willReturn($service_definition['arguments']);
$definition->getProperties()->willReturn($service_definition['properties']);
$definition->getMethodCalls()->willReturn($service_definition['calls']);
$definition->getScope()->willReturn($service_definition['scope']);
$definition->isShared()->willReturn($service_definition['shared']);
$definition->getDecoratedService()->willReturn(NULL);
$definition->getFactory()->willReturn($service_definition['factory']);
$definition->getConfigurator()->willReturn($service_definition['configurator']);
// Preserve order.
$filtered_service_definition = [];
foreach ($base_service_definition as $key => $value) {
$filtered_service_definition[$key] = $service_definition[$key];
unset($service_definition[$key]);
if ($key == 'class' || $key == 'arguments_count') {
continue;
}
if ($filtered_service_definition[$key] === $base_service_definition[$key]) {
unset($filtered_service_definition[$key]);
}
}
// Add remaining properties.
$filtered_service_definition += $service_definition;
// Allow to set _expected values.
foreach (['arguments', 'factory', 'configurator'] as $key) {
$expected = $key . '_expected';
if (isset($filtered_service_definition[$expected])) {
$filtered_service_definition[$key] = $filtered_service_definition[$expected];
unset($filtered_service_definition[$expected]);
}
}
// Remove any remaining scope.
unset($filtered_service_definition['scope']);
if (isset($filtered_service_definition['public']) && $filtered_service_definition['public'] === FALSE) {
$services_provided[] = [
['foo_service' => $definition->reveal()],
[],
];
continue;
}
$services_provided[] = [
['foo_service' => $definition->reveal()],
['foo_service' => $this->serializeDefinition($filtered_service_definition)],
];
}
return $services_provided;
}
/**
* Helper function to serialize a definition.
*
* Used to override serialization.
*/
protected function serializeDefinition(array $service_definition) {
return serialize($service_definition);
}
/**
* Helper function to return a service definition.
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return (object) [
'type' => 'service',
'id' => $id,
'invalidBehavior' => $invalid_behavior,
];
}
/**
* Tests that the correct InvalidArgumentException is thrown for getScope().
*
* @covers ::getServiceDefinition
*/
public function testGetServiceDefinitionWithInvalidScope() {
$bar_definition = new Definition('\stdClass');
$bar_definition->setScope('foo_scope');
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->setExpectedException(InvalidArgumentException::class);
$this->dumper->getArray();
}
/**
* Tests that references to aliases work correctly.
*
* @covers ::getReferenceCall
*
* @dataProvider publicPrivateDataProvider
*/
public function testGetServiceDefinitionWithReferenceToAlias($public) {
$bar_definition = new Definition('\stdClass');
$bar_definition_php_array = [
'class' => '\stdClass',
];
if (!$public) {
$bar_definition->setPublic(FALSE);
$bar_definition_php_array['public'] = FALSE;
}
$bar_definition_php_array['arguments_count'] = 0;
$services['bar'] = $bar_definition;
$aliases['bar.alias'] = 'bar';
$foo = new Definition('\stdClass');
$foo->addArgument(new Reference('bar.alias'));
$services['foo'] = $foo;
$this->containerBuilder->getAliases()->willReturn($aliases);
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->containerBuilder->getDefinition('bar')->willReturn($bar_definition);
$dump = $this->dumper->getArray();
if ($public) {
$service_definition = $this->getServiceCall('bar');
}
else {
$service_definition = $this->getPrivateServiceCall('bar', $bar_definition_php_array, TRUE);
}
$data = [
'class' => '\stdClass',
'arguments' => $this->getCollection([
$service_definition,
]),
'arguments_count' => 1,
];
$this->assertEquals($this->serializeDefinition($data), $dump['services']['foo'], 'Expected definition matches dump.');
}
public function publicPrivateDataProvider() {
return [
[TRUE],
[FALSE],
];
}
/**
* Tests that getDecoratedService() is unsupported.
*
* Tests that the correct InvalidArgumentException is thrown for
* getDecoratedService().
*
* @covers ::getServiceDefinition
*/
public function testGetServiceDefinitionForDecoratedService() {
$bar_definition = new Definition('\stdClass');
$bar_definition->setDecoratedService(new Reference('foo'));
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->setExpectedException(InvalidArgumentException::class);
$this->dumper->getArray();
}
/**
* Tests that the correct RuntimeException is thrown for expressions.
*
* @covers ::dumpValue
*/
public function testGetServiceDefinitionForExpression() {
$expression = new Expression();
$bar_definition = new Definition('\stdClass');
$bar_definition->addArgument($expression);
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->setExpectedException(RuntimeException::class);
$this->dumper->getArray();
}
/**
* Tests that the correct RuntimeException is thrown for dumping an object.
*
* @covers ::dumpValue
*/
public function testGetServiceDefinitionForObject() {
$service = new \stdClass();
$bar_definition = new Definition('\stdClass');
$bar_definition->addArgument($service);
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->setExpectedException(RuntimeException::class);
$this->dumper->getArray();
}
/**
* Tests that the correct RuntimeException is thrown for dumping a resource.
*
* @covers ::dumpValue
*/
public function testGetServiceDefinitionForResource() {
$resource = fopen('php://memory', 'r');
$bar_definition = new Definition('\stdClass');
$bar_definition->addArgument($resource);
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->setExpectedException(RuntimeException::class);
$this->dumper->getArray();
}
/**
* Helper function to return a private service definition.
*/
protected function getPrivateServiceCall($id, $service_definition, $shared = FALSE) {
if (!$id) {
$hash = Crypt::hashBase64(serialize($service_definition));
$id = 'private__' . $hash;
}
return (object) [
'type' => 'private_service',
'id' => $id,
'value' => $service_definition,
'shared' => $shared,
];
}
/**
* Helper function to return a machine-optimized collection.
*/
protected function getCollection($collection, $resolve = TRUE) {
return (object) [
'type' => 'collection',
'value' => $collection,
'resolve' => $resolve,
];
}
/**
* Helper function to return a parameter definition.
*/
protected function getParameterCall($name) {
return (object) [
'type' => 'parameter',
'name' => $name,
];
}
}
}
/**
* As Drupal Core does not ship with ExpressionLanguage component we need to
* define a dummy, else it cannot be tested.
*/
namespace Symfony\Component\ExpressionLanguage {
if (!class_exists('\Symfony\Component\ExpressionLanguage\Expression')) {
/**
* Dummy class to ensure non-existent Symfony component can be tested.
*/
class Expression {
/**
* Gets the string representation of the expression.
*/
public function __toString() {
return 'dummy_expression';
}
}
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\Tests\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper
* @group DependencyInjection
*/
class PhpArrayDumperTest extends OptimizedPhpArrayDumperTest {
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->machineFormat = FALSE;
$this->dumperClass = '\Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper';
parent::setUp();
}
/**
* {@inheritdoc}
*/
protected function serializeDefinition(array $service_definition) {
return $service_definition;
}
/**
* {@inheritdoc}
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return sprintf('@?%s', $id);
}
return sprintf('@%s', $id);
}
/**
* {@inheritdoc}
*/
protected function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* {@inheritdoc}
*/
protected function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}
@@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains a test function for container 'file' include testing.
*/
/**
* Test function for container testing.
*
* @return string
* A string just for testing.
*/
function container_test_file_service_test_service_function() {
return 'Hello Container';
}
@@ -0,0 +1,48 @@
<?php
namespace Drupal\Tests\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\PhpArrayContainer
* @group DependencyInjection
*/
class PhpArrayContainerTest extends ContainerTest {
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->machineFormat = FALSE;
$this->containerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer';
$this->containerDefinition = $this->getMockContainerDefinition();
$this->container = new $this->containerClass($this->containerDefinition);
}
/**
* Helper function to return a service definition.
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return sprintf('@?%s', $id);
}
return sprintf('@%s', $id);
}
/**
* Helper function to return a service definition.
*/
protected function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* Helper function to return a machine-optimized '@notation'.
*/
protected function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Drupal\Tests\Component\Diff;
use Drupal\Component\Diff\Diff;
use Drupal\Component\Diff\DiffFormatter;
/**
* Test DiffFormatter classes.
*
* @coversDefaultClass \Drupal\Component\Diff\DiffFormatter
*
* @group Diff
*/
class DiffFormatterTest extends \PHPUnit_Framework_TestCase {
/**
* @return array
* - Expected formatted diff output.
* - First array of text to diff.
* - Second array of text to diff.
*/
public function provideTestDiff() {
return [
'empty' => ['', [], []],
'add' => [
"3a3\n> line2a\n",
['line1', 'line2', 'line3'],
['line1', 'line2', 'line2a', 'line3'],
],
'delete' => [
"3d3\n< line2a\n",
['line1', 'line2', 'line2a', 'line3'],
['line1', 'line2', 'line3'],
],
'change' => [
"3c3\n< line2a\n---\n> line2b\n",
['line1', 'line2', 'line2a', 'line3'],
['line1', 'line2', 'line2b', 'line3'],
],
];
}
/**
* Tests whether op classes returned by DiffEngine::diff() match expectations.
*
* @covers ::format
* @dataProvider provideTestDiff
*/
public function testDiff($expected, $from, $to) {
$diff = new Diff($from, $to);
$formatter = new DiffFormatter();
$output = $formatter->format($diff);
$this->assertEquals($expected, $output);
}
}
@@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\Component\Diff\Engine;
use Drupal\Component\Diff\Engine\DiffEngine;
use Drupal\Component\Diff\Engine\DiffOpAdd;
use Drupal\Component\Diff\Engine\DiffOpCopy;
use Drupal\Component\Diff\Engine\DiffOpChange;
use Drupal\Component\Diff\Engine\DiffOpDelete;
/**
* Test DiffEngine class.
*
* @coversDefaultClass \Drupal\Component\Diff\Engine\DiffEngine
*
* @group Diff
*/
class DiffEngineTest extends \PHPUnit_Framework_TestCase {
/**
* @return array
* - Expected output in terms of return class. A list of class names
* expected to be returned by DiffEngine::diff().
* - An array of strings to change from.
* - An array of strings to change to.
*/
public function provideTestDiff() {
return [
'empty' => [[], [], []],
'add' => [[DiffOpAdd::class], [], ['a']],
'copy' => [[DiffOpCopy::class], ['a'], ['a']],
'change' => [[DiffOpChange::class], ['a'], ['b']],
'copy-and-change' => [
[
DiffOpCopy::class,
DiffOpChange::class,
],
['a', 'b'],
['a', 'c'],
],
'copy-change-copy' => [
[
DiffOpCopy::class,
DiffOpChange::class,
DiffOpCopy::class,
],
['a', 'b', 'd'],
['a', 'c', 'd'],
],
'copy-change-copy-add' => [
[
DiffOpCopy::class,
DiffOpChange::class,
DiffOpCopy::class,
DiffOpAdd::class,
],
['a', 'b', 'd'],
['a', 'c', 'd', 'e'],
],
'copy-delete' => [
[
DiffOpCopy::class,
DiffOpDelete::class,
],
['a', 'b', 'd'],
['a'],
],
];
}
/**
* Tests whether op classes returned by DiffEngine::diff() match expectations.
*
* @covers ::diff
* @dataProvider provideTestDiff
*/
public function testDiff($expected, $from, $to) {
$diff_engine = new DiffEngine();
$diff = $diff_engine->diff($from, $to);
// Make sure we have the same number of results as expected.
$this->assertCount(count($expected), $diff);
// Make sure the diff objects match our expectations.
foreach ($expected as $index => $op_class) {
$this->assertEquals($op_class, get_class($diff[$index]));
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\Component\Diff\Engine;
use Drupal\Component\Diff\Engine\DiffOp;
/**
* Test DiffOp base class.
*
* The only significant behavior here is that ::reverse() should throw an error
* if not overridden. In versions of this code in other projects, reverse() is
* marked as abstract, which enforces some of this behavior.
*
* @coversDefaultClass \Drupal\Component\Diff\Engine\DiffOp
*
* @group Diff
*/
class DiffOpTest extends \PHPUnit_Framework_TestCase {
/**
* DiffOp::reverse() always throws an error.
*
* @covers ::reverse
*/
public function testReverse() {
$this->setExpectedException(\PHPUnit_Framework_Error::class);
$op = new DiffOp();
$result = $op->reverse();
}
}
@@ -0,0 +1,52 @@
<?php
namespace Drupal\Tests\Component\Diff\Engine;
use Drupal\Component\Diff\Engine\HWLDFWordAccumulator;
/**
* Test HWLDFWordAccumulator.
*
* @coversDefaultClass \Drupal\Component\Diff\Engine\HWLDFWordAccumulator
*
* @group Diff
*/
class HWLDFWordAccumulatorTest extends \PHPUnit_Framework_TestCase {
/**
* Verify that we only get back a NBSP from an empty accumulator.
*
* @covers ::getLines
*
* @see Drupal\Component\Diff\Engine\HWLDFWordAccumulator::NBSP
*/
public function testGetLinesEmpty() {
$acc = new HWLDFWordAccumulator();
$this->assertEquals(['&#160;'], $acc->getLines());
}
/**
* @return array
* - Expected array of lines from getLines().
* - Array of strings for the $words parameter to addWords().
* - String tag for the $tag parameter to addWords().
*/
public function provideAddWords() {
return [
[['wordword2'], ['word', 'word2'], 'tag'],
[['word', 'word2'], ['word', "\nword2"], 'tag'],
[['&#160;', 'word2'], ['', "\nword2"], 'tag'],
];
}
/**
* @covers ::addWords
* @dataProvider provideAddWords
*/
public function testAddWords($expected, $words, $tag) {
$acc = new HWLDFWordAccumulator();
$acc->addWords($words, $tag);
$this->assertEquals($expected, $acc->getLines());
}
}
@@ -0,0 +1,152 @@
<?php
namespace Drupal\Tests\Component\Discovery;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Component\Discovery\YamlDirectoryDiscovery;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* YamlDirectoryDiscoveryTest component unit tests.
*
* @coversDefaultClass \Drupal\Component\Discovery\YamlDirectoryDiscovery
*
* @group Discovery
*/
class YamlDirectoryDiscoveryTest extends UnitTestCase {
/**
* Tests YAML directory discovery.
*
* @covers ::findAll
*/
public function testDiscovery() {
vfsStream::setup('modules', NULL, [
'test_1' => [
'subdir1' => [
'item_1.test.yml' => "id: item1\nname: 'test1 item 1'",
],
'subdir2' => [
'item_2.test.yml' => "id: item2\nname: 'test1 item 2'",
],
],
'test_2' => [
'subdir1' => [
'item_3.test.yml' => "id: item3\nname: 'test2 item 3'",
],
'subdir2' => [],
],
'test_3' => [],
'test_4' => [
'subdir1' => [
'item_4.test.yml' => "id: item4\nname: 'test4 item 4'",
'item_5.test.yml' => "id: item5\nname: 'test4 item 5'",
'item_6.test.yml' => "id: item6\nname: 'test4 item 6'",
],
],
]);
// Set up the directories to search.
$directories = [
// Multiple directories both with valid items.
'test_1' => [
vfsStream::url('modules/test_1/subdir1'),
vfsStream::url('modules/test_1/subdir2'),
],
// The subdir2 directory is empty.
'test_2' => [
vfsStream::url('modules/test_2/subdir1'),
vfsStream::url('modules/test_2/subdir2'),
],
// Directories that do not exist.
'test_3' => [
vfsStream::url('modules/test_3/subdir1'),
vfsStream::url('modules/test_3/subdir2'),
],
// A single directory.
'test_4' => vfsStream::url('modules/test_4/subdir1'),
];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$data = $discovery->findAll();
$this->assertSame(['id' => 'item1', 'name' => 'test1 item 1', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/subdir1/item_1.test.yml'], $data['test_1']['item1']);
$this->assertSame(['id' => 'item2', 'name' => 'test1 item 2', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/subdir2/item_2.test.yml'], $data['test_1']['item2']);
$this->assertCount(2, $data['test_1']);
$this->assertSame(['id' => 'item3', 'name' => 'test2 item 3', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_2/subdir1/item_3.test.yml'], $data['test_2']['item3']);
$this->assertCount(1, $data['test_2']);
$this->assertTrue(empty($data['test_3']), 'test_3 provides 0 items');
$this->assertSame(['id' => 'item4', 'name' => 'test4 item 4', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1/item_4.test.yml'], $data['test_4']['item4']);
$this->assertSame(['id' => 'item5', 'name' => 'test4 item 5', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1/item_5.test.yml'], $data['test_4']['item5']);
$this->assertSame(['id' => 'item6', 'name' => 'test4 item 6', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1/item_6.test.yml'], $data['test_4']['item6']);
$this->assertCount(3, $data['test_4']);
}
/**
* Tests YAML directory discovery with an alternate ID key.
*
* @covers ::findAll
*/
public function testDiscoveryAlternateId() {
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "alt_id: item1\nid: ignored",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test', 'alt_id');
$data = $discovery->findAll();
$this->assertSame(['alt_id' => 'item1', 'id' => 'ignored', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/item_1.test.yml'], $data['test_1']['item1']);
$this->assertCount(1, $data['test_1']);
}
/**
* Tests YAML directory discovery with a missing ID key.
*
* @covers ::findAll
* @covers ::getIdentifier
*/
public function testDiscoveryNoIdException() {
$this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\'');
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->findAll();
}
/**
* Tests YAML directory discovery with invalid YAML.
*
* @covers ::findAll
*/
public function testDiscoveryInvalidYamlException() {
$this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains invalid YAML');
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "id: invalid\nfoo : [bar}",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->findAll();
}
}
@@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\Component\Discovery;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Discovery\YamlDiscovery;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamWrapper;
use org\bovigo\vfs\vfsStreamDirectory;
/**
* YamlDiscovery component unit tests.
*
* @group Discovery
*/
class YamlDiscoveryTest extends UnitTestCase {
/**
* Tests the YAML file discovery.
*/
public function testDiscovery() {
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('modules');
vfsStreamWrapper::setRoot($root);
$url = vfsStream::url('modules');
mkdir($url . '/test_1');
file_put_contents($url . '/test_1/test_1.test.yml', 'name: test');
file_put_contents($url . '/test_1/test_2.test.yml', 'name: test');
mkdir($url . '/test_2');
file_put_contents($url . '/test_2/test_3.test.yml', 'name: test');
// Write an empty YAML file.
file_put_contents($url . '/test_2/test_4.test.yml', '');
// Set up the directories to search.
$directories = [
'test_1' => $url . '/test_1',
'test_2' => $url . '/test_1',
'test_3' => $url . '/test_2',
'test_4' => $url . '/test_2',
];
$discovery = new YamlDiscovery('test', $directories);
$data = $discovery->findAll();
$this->assertEquals(count($data), count($directories));
$this->assertArrayHasKey('test_1', $data);
$this->assertArrayHasKey('test_2', $data);
$this->assertArrayHasKey('test_3', $data);
$this->assertArrayHasKey('test_4', $data);
foreach (['test_1', 'test_2', 'test_3'] as $key) {
$this->assertArrayHasKey('name', $data[$key]);
$this->assertEquals($data[$key]['name'], 'test');
}
$this->assertSame([], $data['test_4']);
}
}
@@ -0,0 +1,127 @@
<?php
namespace Drupal\Tests\Component;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* General tests for \Drupal\Component that can't go anywhere else.
*
* @group Component
*/
class DrupalComponentTest extends UnitTestCase {
/**
* Tests that classes in Component do not use any Core class.
*/
public function testNoCoreInComponent() {
$component_path = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))) . '/lib/Drupal/Component';
foreach ($this->findPhpClasses($component_path) as $class) {
$this->assertNoCoreUsage($class);
}
}
/**
* Tests that classes in Component Tests do not use any Core class.
*/
public function testNoCoreInComponentTests() {
$component_path = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))) . '/tests/Drupal/Tests/Component';
foreach ($this->findPhpClasses($component_path) as $class) {
$this->assertNoCoreUsage($class);
}
}
/**
* Searches a directory recursively for PHP classes.
*
* @param string $dir
* The full path to the directory that should be checked.
*
* @return array
* An array of class paths.
*/
protected function findPhpClasses($dir) {
$classes = [];
foreach (new \DirectoryIterator($dir) as $file) {
if ($file->isDir() && !$file->isDot()) {
$classes = array_merge($classes, $this->findPhpClasses($file->getPathname()));
}
elseif ($file->getExtension() == 'php') {
$classes[] = $file->getPathname();
}
}
return $classes;
}
/**
* Asserts that the given class is not using any class from Core namespace.
*
* @param string $class_path
* The full path to the class that should be checked.
*/
protected function assertNoCoreUsage($class_path) {
$contents = file_get_contents($class_path);
preg_match_all('/^.*Drupal\\\Core.*$/m', $contents, $matches);
$matches = array_filter($matches[0], function($line) {
// Filter references to @see as they don't really matter.
return strpos($line, '@see') === FALSE;
});
$this->assertEmpty($matches, "Checking for illegal reference to 'Drupal\\Core' namespace in $class_path");
}
/**
* Data provider for testAssertNoCoreUseage().
*
* @return array
* Data for testAssertNoCoreUseage() in the form:
* - TRUE if the test passes, FALSE otherwise.
* - File data as a string. This will be used as a virtual file.
*/
public function providerAssertNoCoreUseage() {
return [
[
TRUE,
'@see \\Drupal\\Core\\Something',
],
[
FALSE,
'\\Drupal\\Core\\Something',
],
[
FALSE,
"@see \\Drupal\\Core\\Something\n" .
'\\Drupal\\Core\\Something',
],
[
FALSE,
"\\Drupal\\Core\\Something\n" .
'@see \\Drupal\\Core\\Something',
],
];
}
/**
* @covers \Drupal\Tests\Component\DrupalComponentTest::assertNoCoreUsage
* @dataProvider providerAssertNoCoreUseage
*/
public function testAssertNoCoreUseage($expected_pass, $file_data) {
// Set up a virtual file to read.
$vfs_root = vfsStream::setup('root');
vfsStream::newFile('Test.php')->at($vfs_root)->setContent($file_data);
$file_uri = vfsStream::url('root/Test.php');
try {
$pass = TRUE;
$this->assertNoCoreUsage($file_uri);
}
catch (\PHPUnit_Framework_AssertionFailedError $e) {
$pass = FALSE;
}
$this->assertEquals($expected_pass, $pass, $expected_pass ?
'Test caused a false positive' :
'Test failed to detect Core usage');
}
}
@@ -0,0 +1,195 @@
<?php
// @codingStandardsIgnoreFile
namespace Drupal\Tests\Component\EventDispatcher;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\Tests\CallableClass;
use Symfony\Component\EventDispatcher\Tests\TestEventListener;
use Symfony\Component\EventDispatcher\Tests\ContainerAwareEventDispatcherTest as SymfonyContainerAwareEventDispatcherTest;
/**
* Unit tests for the ContainerAwareEventDispatcher.
*
* NOTE: 98% of this code is a literal copy of Symfony's emerging
* CompiledEventDispatcherTest.
*
* This file does NOT follow Drupal coding standards, so as to simplify future
* synchronizations.
*
* @see https://github.com/symfony/symfony/pull/12521
*
* @group EventDispatcher
*/
class ContainerAwareEventDispatcherTest extends SymfonyContainerAwareEventDispatcherTest
{
protected function createEventDispatcher()
{
$container = new Container();
return new ContainerAwareEventDispatcher($container);
}
public function testGetListenersWithCallables()
{
// When passing in callables exclusively as listeners into the event
// dispatcher constructor, the event dispatcher must not attempt to
// resolve any services.
$container = $this->getMock('Symfony\Component\DependencyInjection\IntrospectableContainerInterface');
$container->expects($this->never())->method($this->anything());
$firstListener = new CallableClass();
$secondListener = function () {};
$thirdListener = array(new TestEventListener(), 'preFoo');
$listeners = array(
'test_event' => array(
0 => array(
array('callable' => $firstListener),
array('callable' => $secondListener),
array('callable' => $thirdListener),
),
),
);
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$actualListeners = $dispatcher->getListeners();
$expectedListeners = array(
'test_event' => array(
$firstListener,
$secondListener,
$thirdListener,
),
);
$this->assertSame($expectedListeners, $actualListeners);
}
public function testDispatchWithCallables()
{
// When passing in callables exclusively as listeners into the event
// dispatcher constructor, the event dispatcher must not attempt to
// resolve any services.
$container = $this->getMock('Symfony\Component\DependencyInjection\IntrospectableContainerInterface');
$container->expects($this->never())->method($this->anything());
$firstListener = new CallableClass();
$secondListener = function () {};
$thirdListener = array(new TestEventListener(), 'preFoo');
$listeners = array(
'test_event' => array(
0 => array(
array('callable' => $firstListener),
array('callable' => $secondListener),
array('callable' => $thirdListener),
),
),
);
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$dispatcher->dispatch('test_event');
$this->assertTrue($thirdListener[0]->preFooInvoked);
}
public function testGetListenersWithServices()
{
$container = new ContainerBuilder();
$container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener');
$listeners = array(
'test_event' => array(
0 => array(
array('service' => array('listener_service', 'preFoo')),
),
),
);
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$actualListeners = $dispatcher->getListeners();
$listenerService = $container->get('listener_service');
$expectedListeners = array(
'test_event' => array(
array($listenerService, 'preFoo'),
),
);
$this->assertSame($expectedListeners, $actualListeners);
}
public function testDispatchWithServices()
{
$container = new ContainerBuilder();
$container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener');
$listeners = array(
'test_event' => array(
0 => array(
array('service' => array('listener_service', 'preFoo')),
),
),
);
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$dispatcher->dispatch('test_event');
$listenerService = $container->get('listener_service');
$this->assertTrue($listenerService->preFooInvoked);
}
public function testRemoveService()
{
$container = new ContainerBuilder();
$container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener');
$container->register('other_listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener');
$listeners = array(
'test_event' => array(
0 => array(
array('service' => array('listener_service', 'preFoo')),
array('service' => array('other_listener_service', 'preFoo')),
),
),
);
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$listenerService = $container->get('listener_service');
$dispatcher->removeListener('test_event', array($listenerService, 'preFoo'));
// Ensure that other service was not initialized during removal of the
// listener service.
$this->assertFalse($container->initialized('other_listener_service'));
$dispatcher->dispatch('test_event');
$this->assertFalse($listenerService->preFooInvoked);
$otherService = $container->get('other_listener_service');
$this->assertTrue($otherService->preFooInvoked);
}
public function testGetListenerPriorityWithServices()
{
$container = new ContainerBuilder();
$container->register('listener_service', TestEventListener::class);
$listeners = array(
'test_event' => array(
5 => array(
array('service' => array('listener_service', 'preFoo')),
),
),
);
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$listenerService = $container->get('listener_service');
$actualPriority = $dispatcher->getListenerPriority('test_event', [$listenerService, 'preFoo']);
$this->assertSame(5, $actualPriority);
}
}
@@ -0,0 +1,178 @@
<?php
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCache;
use Drupal\Component\FileCache\NullFileCache;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\FileCache\FileCacheFactory
* @group FileCache
*/
class FileCacheFactoryTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$configuration = [
'test_foo_settings' => [
'collection' => 'test-23',
'cache_backend_class' => '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend',
'cache_backend_configuration' => [
'bin' => 'dog',
],
],
];
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix('prefix');
}
/**
* @covers ::get
*/
public function testGet() {
$file_cache = FileCacheFactory::get('test_foo_settings', []);
// Ensure the right backend and configuration is used.
$filename = __DIR__ . '/Fixtures/llama-23.txt';
$realpath = realpath($filename);
$cid = 'prefix:test-23:' . $realpath;
$file_cache->set($filename, 23);
$static_cache = new StaticFileCacheBackend(['bin' => 'dog']);
$result = $static_cache->fetch([$cid]);
$this->assertNotEmpty($result);
// Cleanup static caches.
$file_cache->delete($filename);
}
/**
* @covers ::get
*/
public function testGetNoPrefix() {
FileCacheFactory::setPrefix(NULL);
$this->setExpectedException(\InvalidArgumentException::class, 'Required prefix configuration is missing');
FileCacheFactory::get('test_foo_settings', []);
}
/**
* @covers ::get
*/
public function testGetDisabledFileCache() {
// Ensure the returned FileCache is an instance of FileCache::class.
$file_cache = FileCacheFactory::get('test_foo_settings', []);
$this->assertInstanceOf(FileCache::class, $file_cache);
$configuration = FileCacheFactory::getConfiguration();
$configuration[FileCacheFactory::DISABLE_CACHE] = TRUE;
FileCacheFactory::setConfiguration($configuration);
// Ensure the returned FileCache is now an instance of NullFileCache::class.
$file_cache = FileCacheFactory::get('test_foo_settings', []);
$this->assertInstanceOf(NullFileCache::class, $file_cache);
}
/**
* @covers ::get
*
* @dataProvider configurationDataProvider
*/
public function testGetConfigurationOverrides($configuration, $arguments, $class) {
FileCacheFactory::setConfiguration($configuration);
$file_cache = FileCacheFactory::get('test_foo_settings', $arguments);
$this->assertInstanceOf($class, $file_cache);
}
/**
* Data provider for testGetConfigurationOverrides().
*/
public function configurationDataProvider() {
$data = [];
// Get a unique FileCache class.
$file_cache = $this->getMockBuilder(FileCache::class)
->disableOriginalConstructor()
->getMock();
$class = get_class($file_cache);
// Test fallback configuration.
$data['fallback-configuration'] = [[
], [], FileCache::class];
// Test default configuration.
$data['default-configuration'] = [[
'default' => [
'class' => $class,
],
], [], $class];
// Test specific per collection setting.
$data['collection-setting'] = [[
'test_foo_settings' => [
'class' => $class,
],
], [], $class];
// Test default configuration plus specific per collection setting.
$data['default-plus-collection-setting'] = [[
'default' => [
'class' => '\stdClass',
],
'test_foo_settings' => [
'class' => $class,
],
], [], $class];
// Test default configuration plus class specific override.
$data['default-plus-class-override'] = [[
'default' => [
'class' => '\stdClass',
],
], [ 'class' => $class ], $class];
// Test default configuration plus class specific override plus specific
// per collection setting.
$data['default-plus-class-plus-collection-setting'] = [[
'default' => [
'class' => '\stdClass',
],
'test_foo_settings' => [
'class' => $class,
],
], [ 'class' => '\stdClass'], $class];
return $data;
}
/**
* @covers ::getConfiguration
* @covers ::setConfiguration
*/
public function testGetSetConfiguration() {
$configuration = FileCacheFactory::getConfiguration();
$configuration['test_foo_bar'] = ['bar' => 'llama'];
FileCacheFactory::setConfiguration($configuration);
$configuration = FileCacheFactory::getConfiguration();
$this->assertEquals(['bar' => 'llama'], $configuration['test_foo_bar']);
}
/**
* @covers ::getPrefix
* @covers ::setPrefix
*/
public function testGetSetPrefix() {
$prefix = $this->randomMachineName();
FileCacheFactory::setPrefix($prefix);
$this->assertEquals($prefix, FileCacheFactory::getPrefix());
}
}
@@ -0,0 +1,142 @@
<?php
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCache;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\FileCache\FileCache
* @group FileCache
*/
class FileCacheTest extends UnitTestCase {
/**
* FileCache object used for the tests.
*
* @var \Drupal\Component\FileCache\FileCacheInterface
*/
protected $fileCache;
/**
* Static FileCache object used for verification of tests.
*
* @var \Drupal\Component\FileCache\FileCacheBackendInterface
*/
protected $staticFileCache;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->fileCache = new FileCache('prefix', 'test', '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend', ['bin' => 'llama']);
$this->staticFileCache = new StaticFileCacheBackend(['bin' => 'llama']);
}
/**
* @covers ::get
* @covers ::__construct
*/
public function testGet() {
// Test a cache miss.
$result = $this->fileCache->get(__DIR__ . '/Fixtures/no-llama-42.yml');
$this->assertNull($result);
// Test a cache hit.
$filename = __DIR__ . '/Fixtures/llama-42.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$data = [
'mtime' => filemtime($realpath),
'filepath' => $realpath,
'data' => 42,
];
$this->staticFileCache->store($cid, $data);
$result = $this->fileCache->get($filename);
$this->assertEquals(42, $result);
// Cleanup static caches.
$this->fileCache->delete($filename);
}
/**
* @covers ::getMultiple
*/
public function testGetMultiple() {
// Test a cache miss.
$result = $this->fileCache->getMultiple([__DIR__ . '/Fixtures/no-llama-42.yml']);
$this->assertEmpty($result);
// Test a cache hit.
$filename = __DIR__ . '/Fixtures/llama-42.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$data = [
'mtime' => filemtime($realpath),
'filepath' => $realpath,
'data' => 42,
];
$this->staticFileCache->store($cid, $data);
$result = $this->fileCache->getMultiple([$filename]);
$this->assertEquals([$filename => 42], $result);
// Test a static cache hit.
$file2 = __DIR__ . '/Fixtures/llama-23.txt';
$this->fileCache->set($file2, 23);
$result = $this->fileCache->getMultiple([$filename, $file2]);
$this->assertEquals([$filename => 42, $file2 => 23], $result);
// Cleanup static caches.
$this->fileCache->delete($filename);
$this->fileCache->delete($file2);
}
/**
* @covers ::set
*/
public function testSet() {
$filename = __DIR__ . '/Fixtures/llama-23.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$data = [
'mtime' => filemtime($realpath),
'filepath' => $realpath,
'data' => 23,
];
$this->fileCache->set($filename, 23);
$result = $this->staticFileCache->fetch([$cid]);
$this->assertEquals([$cid => $data], $result);
// Cleanup static caches.
$this->fileCache->delete($filename);
}
/**
* @covers ::delete
*/
public function testDelete() {
$filename = __DIR__ . '/Fixtures/llama-23.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$this->fileCache->set($filename, 23);
// Ensure data is removed after deletion.
$this->fileCache->delete($filename);
$result = $this->staticFileCache->fetch([$cid]);
$this->assertEquals([], $result);
$result = $this->fileCache->get($filename);
$this->assertNull($result);
}
}
@@ -0,0 +1 @@
23
@@ -0,0 +1 @@
42
@@ -0,0 +1,71 @@
<?php
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCacheBackendInterface;
/**
* Allows to cache data based on file modification dates in a static cache.
*/
class StaticFileCacheBackend implements FileCacheBackendInterface {
/**
* Internal static cache.
*
* @var array
*/
protected static $cache = [];
/**
* Bin used for storing the data in the static cache.
*
* @var string
*/
protected $bin;
/**
* Constructs a PHP Storage FileCache backend.
*
* @param array $configuration
* (optional) Configuration used to configure this object.
*/
public function __construct($configuration) {
$this->bin = isset($configuration['bin']) ? $configuration['bin'] : 'file_cache';
}
/**
* {@inheritdoc}
*/
public function fetch(array $cids) {
$result = [];
foreach ($cids as $cid) {
if (isset(static::$cache[$this->bin][$cid])) {
$result[$cid] = static::$cache[$this->bin][$cid];
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function store($cid, $data) {
static::$cache[$this->bin][$cid] = $data;
}
/**
* {@inheritdoc}
*/
public function delete($cid) {
unset(static::$cache[$this->bin][$cid]);
}
/**
* Allows tests to reset the static cache to avoid side effects.
*/
public static function reset() {
static::$cache = [];
}
}
@@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\Component\FileSystem;
use Drupal\Component\FileSystem\RegexDirectoryIterator;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* @coversDefaultClass \Drupal\Component\FileSystem\RegexDirectoryIterator
* @group FileSystem
*/
class RegexDirectoryIteratorTest extends UnitTestCase {
/**
* @covers ::accept
* @dataProvider providerTestRegexDirectoryIterator
*/
public function testRegexDirectoryIterator(array $directory, $regex, array $expected) {
vfsStream::setup('root', NULL, $directory);
$iterator = new RegexDirectoryIterator(vfsStream::url('root'), $regex);
// Create an array of filenames to assert against.
$file_list = array_map(function(\SplFileInfo $file) {
return $file->getFilename();
}, array_values(iterator_to_array($iterator)));
$this->assertSame($expected, $file_list);
}
/**
* Provider for self::testRegexDirectoryIterator().
*/
public function providerTestRegexDirectoryIterator() {
return [
[
[
'1.yml' => '',
],
'/\.yml$/',
[
'1.yml',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/\.yml$/',
[
'1.yml',
'2.yml',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/\.txt/',
[
'3.txt',
],
],
[
[
'1.yml' => '',
// Ensure we don't recurse in directories even if that match the
// regex.
'2.yml' => [
'3.yml' => '',
'4.yml' => '',
],
'3.txt' => '',
],
'/\.yml$/',
[
'1.yml',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/^\d/',
[
'1.yml',
'2.yml',
'3.txt'
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/^\D/',
[],
],
];
}
}
@@ -0,0 +1,370 @@
<?php
namespace Drupal\Tests\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Tests\UnitTestCase;
/**
* Unit tests for the Gettext PO file header handling features.
*
* @see Drupal\Component\Gettext\PoHeader.
*
* @group Gettext
*/
class PoHeaderTest extends UnitTestCase {
/**
* Tests that plural expressions are evaluated correctly.
*
* Validate that the given plural expressions is evaluated with the correct
* plural formula.
*
* @param string $plural
* The plural expression.
* @param array $expected
* Array of expected plural positions keyed by plural value.
*
* @dataProvider providerTestPluralsFormula
*/
public function testPluralsFormula($plural, $expected) {
$p = new PoHeader();
$parsed = $p->parsePluralForms($plural);
list($nplurals, $new_plural) = $parsed;
foreach ($expected as $number => $plural_form) {
$result = isset($new_plural[$number]) ? $new_plural[$number] : $new_plural['default'];
$this->assertEquals($result, $plural_form, 'Difference found at ' . $number . ': ' . $plural_form . ' versus ' . $result);
}
}
/**
* Data provider for testPluralsFormula.
*
* Gets pairs of plural expressions and expected plural positions keyed by
* plural value.
*
* @return array
* Pairs of plural expressions and expected plural positions keyed by plural
* value.
*/
public function providerTestPluralsFormula() {
return [
[
'nplurals=1; plural=0;',
['default' => 0],
],
[
'nplurals=2; plural=(n > 1);',
[0 => 0, 1 => 0, 'default' => 1],
],
[
'nplurals=2; plural=(n!=1);',
[1 => 0, 'default' => 1],
],
[
'nplurals=2; plural=(((n==1)||((n%10)==1))?(0):1);',
[
1 => 0,
11 => 0,
21 => 0,
31 => 0,
41 => 0,
51 => 0,
61 => 0,
71 => 0,
81 => 0,
91 => 0,
101 => 0,
111 => 0,
121 => 0,
131 => 0,
141 => 0,
151 => 0,
161 => 0,
171 => 0,
181 => 0,
191 => 0,
'default' => 1,
],
],
[
'nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));',
[
1 => 0,
2 => 1,
3 => 1,
4 => 1,
21 => 0,
22 => 1,
23 => 1,
24 => 1,
31 => 0,
32 => 1,
33 => 1,
34 => 1,
41 => 0,
42 => 1,
43 => 1,
44 => 1,
51 => 0,
52 => 1,
53 => 1,
54 => 1,
61 => 0,
62 => 1,
63 => 1,
64 => 1,
71 => 0,
72 => 1,
73 => 1,
74 => 1,
81 => 0,
82 => 1,
83 => 1,
84 => 1,
91 => 0,
92 => 1,
93 => 1,
94 => 1,
101 => 0,
102 => 1,
103 => 1,
104 => 1,
121 => 0,
122 => 1,
123 => 1,
124 => 1,
131 => 0,
132 => 1,
133 => 1,
134 => 1,
141 => 0,
142 => 1,
143 => 1,
144 => 1,
151 => 0,
152 => 1,
153 => 1,
154 => 1,
161 => 0,
162 => 1,
163 => 1,
164 => 1,
171 => 0,
172 => 1,
173 => 1,
174 => 1,
181 => 0,
182 => 1,
183 => 1,
184 => 1,
191 => 0,
192 => 1,
193 => 1,
194 => 1,
'default' => 2,
],
],
[
'nplurals=3; plural=((n==1)?(0):(((n>=2)&&(n<=4))?(1):2));',
[
1 => 0,
2 => 1,
3 => 1,
4 => 1,
'default' => 2,
],
],
[
'nplurals=3; plural=((n==1)?(0):(((n==0)||(((n%100)>0)&&((n%100)<20)))?(1):2));',
[
0 => 1,
1 => 0,
2 => 1,
3 => 1,
4 => 1,
5 => 1,
6 => 1,
7 => 1,
8 => 1,
9 => 1,
10 => 1,
11 => 1,
12 => 1,
13 => 1,
14 => 1,
15 => 1,
16 => 1,
17 => 1,
18 => 1,
19 => 1,
101 => 1,
102 => 1,
103 => 1,
104 => 1,
105 => 1,
106 => 1,
107 => 1,
108 => 1,
109 => 1,
110 => 1,
111 => 1,
112 => 1,
113 => 1,
114 => 1,
115 => 1,
116 => 1,
117 => 1,
118 => 1,
119 => 1,
'default' => 2,
],
],
[
'nplurals=3; plural=((n==1)?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));',
[
1 => 0,
2 => 1,
3 => 1,
4 => 1,
22 => 1,
23 => 1,
24 => 1,
32 => 1,
33 => 1,
34 => 1,
42 => 1,
43 => 1,
44 => 1,
52 => 1,
53 => 1,
54 => 1,
62 => 1,
63 => 1,
64 => 1,
72 => 1,
73 => 1,
74 => 1,
82 => 1,
83 => 1,
84 => 1,
92 => 1,
93 => 1,
94 => 1,
102 => 1,
103 => 1,
104 => 1,
122 => 1,
123 => 1,
124 => 1,
132 => 1,
133 => 1,
134 => 1,
142 => 1,
143 => 1,
144 => 1,
152 => 1,
153 => 1,
154 => 1,
162 => 1,
163 => 1,
164 => 1,
172 => 1,
173 => 1,
174 => 1,
182 => 1,
183 => 1,
184 => 1,
192 => 1,
193 => 1,
194 => 1,
'default' => 2,
], ],
[
'nplurals=4; plural=(((n==1)||(n==11))?(0):(((n==2)||(n==12))?(1):(((n>2)&&(n<20))?(2):3)));',
[
1 => 0,
2 => 1,
3 => 2,
4 => 2,
5 => 2,
6 => 2,
7 => 2,
8 => 2,
9 => 2,
10 => 2,
11 => 0,
12 => 1,
13 => 2,
14 => 2,
15 => 2,
16 => 2,
17 => 2,
18 => 2,
19 => 2,
'default' => 3,
],
],
[
'nplurals=4; plural=(((n%100)==1)?(0):(((n%100)==2)?(1):((((n%100)==3)||((n%100)==4))?(2):3)));',
[
1 => 0,
2 => 1,
3 => 2,
4 => 2,
101 => 0,
102 => 1,
103 => 2,
104 => 2,
'default' => 3,
],
],
[
'nplurals=5; plural=((n==1)?(0):((n==2)?(1):((n<7)?(2):((n<11)?(3):4))));',
[
0 => 2,
1 => 0,
2 => 1,
3 => 2,
4 => 2,
5 => 2,
6 => 2,
7 => 3,
8 => 3,
9 => 3,
10 => 3,
'default' => 4,
],
],
[
'nplurals=6; plural=((n==1)?(0):((n==0)?(1):((n==2)?(2):((((n%100)>=3)&&((n%100)<=10))?(3):((((n%100)>=11)&&((n%100)<=99))?(4):5)))));',
[
0 => 1,
1 => 0,
2 => 2,
3 => 3,
4 => 3,
5 => 3,
6 => 3,
7 => 3,
8 => 3,
9 => 3,
10 => 3,
100 => 5,
101 => 5,
102 => 5,
103 => 3,
104 => 3,
105 => 3,
106 => 3,
107 => 3,
108 => 3,
109 => 3,
110 => 3,
'default' => 4,
],
],
];
}
}
@@ -0,0 +1,191 @@
<?php
namespace Drupal\Tests\Component\Graph;
use Drupal\Component\Graph\Graph;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Graph\Graph
* @group Graph
*/
class GraphTest extends UnitTestCase {
/**
* Test depth-first-search features.
*/
public function testDepthFirstSearch() {
// The sample graph used is:
// 1 --> 2 --> 3 5 ---> 6
// | ^ ^
// | | |
// | | |
// +---> 4 <-- 7 8 ---> 9
$graph = $this->normalizeGraph([
1 => [2],
2 => [3, 4],
3 => [],
4 => [3],
5 => [6],
7 => [4, 5],
8 => [9],
9 => [],
]);
$graph_object = new Graph($graph);
$graph = $graph_object->searchAndSort();
$expected_paths = [
1 => [2, 3, 4],
2 => [3, 4],
3 => [],
4 => [3],
5 => [6],
7 => [4, 3, 5, 6],
8 => [9],
9 => [],
];
$this->assertPaths($graph, $expected_paths);
$expected_reverse_paths = [
1 => [],
2 => [1],
3 => [2, 1, 4, 7],
4 => [2, 1, 7],
5 => [7],
7 => [],
8 => [],
9 => [8],
];
$this->assertReversePaths($graph, $expected_reverse_paths);
// Assert that DFS didn't created "missing" vertexes automatically.
$this->assertFalse(isset($graph[6]), 'Vertex 6 has not been created');
$expected_components = [
[1, 2, 3, 4, 5, 7],
[8, 9],
];
$this->assertComponents($graph, $expected_components);
$expected_weights = [
[1, 2, 3],
[2, 4, 3],
[7, 4, 3],
[7, 5],
[8, 9],
];
$this->assertWeights($graph, $expected_weights);
}
/**
* Normalizes a graph.
*
* @param $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
*
* @return array
* The normalized version of a graph.
*/
protected function normalizeGraph($graph) {
$normalized_graph = [];
foreach ($graph as $vertex => $edges) {
// Create vertex even if it hasn't any edges.
$normalized_graph[$vertex] = [];
foreach ($edges as $edge) {
$normalized_graph[$vertex]['edges'][$edge] = TRUE;
}
}
return $normalized_graph;
}
/**
* Verify expected paths in a graph.
*
* @param $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
* @param $expected_paths
* An associative array containing vertices with their expected paths.
*/
protected function assertPaths($graph, $expected_paths) {
foreach ($expected_paths as $vertex => $paths) {
// Build an array with keys = $paths and values = TRUE.
$expected = array_fill_keys($paths, TRUE);
$result = isset($graph[$vertex]['paths']) ? $graph[$vertex]['paths'] : [];
$this->assertEquals($expected, $result, sprintf('Expected paths for vertex %s: %s, got %s', $vertex, $this->displayArray($expected, TRUE), $this->displayArray($result, TRUE)));
}
}
/**
* Verify expected reverse paths in a graph.
*
* @param $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
* @param $expected_reverse_paths
* An associative array containing vertices with their expected reverse
* paths.
*/
protected function assertReversePaths($graph, $expected_reverse_paths) {
foreach ($expected_reverse_paths as $vertex => $paths) {
// Build an array with keys = $paths and values = TRUE.
$expected = array_fill_keys($paths, TRUE);
$result = isset($graph[$vertex]['reverse_paths']) ? $graph[$vertex]['reverse_paths'] : [];
$this->assertEquals($expected, $result, sprintf('Expected reverse paths for vertex %s: %s, got %s', $vertex, $this->displayArray($expected, TRUE), $this->displayArray($result, TRUE)));
}
}
/**
* Verify expected components in a graph.
*
* @param $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort().
* @param $expected_components
* An array containing of components defined as a list of their vertices.
*/
protected function assertComponents($graph, $expected_components) {
$unassigned_vertices = array_fill_keys(array_keys($graph), TRUE);
foreach ($expected_components as $component) {
$result_components = [];
foreach ($component as $vertex) {
$result_components[] = $graph[$vertex]['component'];
unset($unassigned_vertices[$vertex]);
}
$this->assertEquals(1, count(array_unique($result_components)), sprintf('Expected one unique component for vertices %s, got %s', $this->displayArray($component), $this->displayArray($result_components)));
}
$this->assertEquals([], $unassigned_vertices, sprintf('Vertices not assigned to a component: %s', $this->displayArray($unassigned_vertices, TRUE)));
}
/**
* Verify expected order in a graph.
*
* @param $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
* @param $expected_orders
* An array containing lists of vertices in their expected order.
*/
protected function assertWeights($graph, $expected_orders) {
foreach ($expected_orders as $order) {
$previous_vertex = array_shift($order);
foreach ($order as $vertex) {
$this->assertTrue($graph[$previous_vertex]['weight'] < $graph[$vertex]['weight'], sprintf('Weights of %s and %s are correct relative to each other', $previous_vertex, $vertex));
}
}
}
/**
* Helper function to output vertices as comma-separated list.
*
* @param $paths
* An array containing a list of vertices.
* @param $keys
* (optional) Whether to output the keys of $paths instead of the values.
*/
protected function displayArray($paths, $keys = FALSE) {
if (!empty($paths)) {
return implode(', ', $keys ? array_keys($paths) : $paths);
}
else {
return '(empty)';
}
}
}
@@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\HttpFoundation\SecuredRedirectResponseTest.
*/
namespace Drupal\Tests\Component\HttpFoundation;
use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Test secure redirect base class.
*
* @group Routing
* @coversDefaultClass \Drupal\Component\HttpFoundation\SecuredRedirectResponse
*/
class SecuredRedirectResponseTest extends UnitTestCase {
/**
* Test copying of redirect response.
*
* @covers ::createFromRedirectResponse
* @covers ::fromResponse
*/
public function testRedirectCopy() {
$redirect = new RedirectResponse('/magic_redirect_url', 301, ['x-cache-foobar' => 123]);
$redirect->setProtocolVersion('2.0');
$redirect->setCharset('ibm-943_P14A-2000');
$redirect->headers->setCookie(new Cookie('name', 'value'));
// Make a cloned redirect.
$secureRedirect = SecuredRedirectStub::createFromRedirectResponse($redirect);
$this->assertEquals('/magic_redirect_url', $secureRedirect->getTargetUrl());
$this->assertEquals(301, $secureRedirect->getStatusCode());
// We pull the headers from the original redirect because there are default headers applied.
$headers1 = $redirect->headers->allPreserveCase();
$headers2 = $secureRedirect->headers->allPreserveCase();
// We unset cache headers so we don't test arcane Symfony weirdness.
// https://github.com/symfony/symfony/issues/16171
unset($headers1['Cache-Control'], $headers2['Cache-Control']);
$this->assertEquals($headers1, $headers2);
$this->assertEquals('2.0', $secureRedirect->getProtocolVersion());
$this->assertEquals('ibm-943_P14A-2000', $secureRedirect->getCharset());
$this->assertEquals($redirect->headers->getCookies(), $secureRedirect->headers->getCookies());
}
}
class SecuredRedirectStub extends SecuredRedirectResponse {
/**
* {@inheritdoc}
*/
protected function isSafe($url) {
// Empty implementation for testing.
return TRUE;
}
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Component\PhpStorage\FileReadOnlyStorage;
/**
* @coversDefaultClass \Drupal\Component\PhpStorage\FileReadOnlyStorage
*
* @group Drupal
* @group PhpStorage
*/
class FileStorageReadOnlyTest extends PhpStorageTestBase {
/**
* Standard test settings to pass to storage instances.
*
* @var array
*/
protected $standardSettings;
/**
* Read only test settings to pass to storage instances.
*
* @var array
*/
protected $readonlyStorage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->standardSettings = [
'directory' => $this->directory,
'bin' => 'test',
];
$this->readonlyStorage = [
'directory' => $this->directory,
// Let this read from the bin where the other instance is writing.
'bin' => 'test',
];
}
/**
* Tests writing with one class and reading with another.
*/
public function testReadOnly() {
$php = new FileStorage($this->standardSettings);
$name = $this->randomMachineName() . '/' . $this->randomMachineName() . '.php';
// Find a global that doesn't exist.
do {
$random = mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write out a PHP file and ensure it's successfully loaded.
$code = "<?php\n\$GLOBALS[$random] = TRUE;";
$success = $php->save($name, $code);
$this->assertSame($success, TRUE);
$php_read = new FileReadOnlyStorage($this->readonlyStorage);
$php_read->load($name);
$this->assertTrue($GLOBALS[$random]);
// If the file was successfully loaded, it must also exist, but ensure the
// exists() method returns that correctly.
$this->assertSame($php_read->exists($name), TRUE);
// Saving and deleting should always fail.
$this->assertFalse($php_read->save($name, $code));
$this->assertFalse($php_read->delete($name));
unset($GLOBALS[$random]);
}
/**
* @covers ::writeable
*/
public function testWriteable() {
$php_read = new FileReadOnlyStorage($this->readonlyStorage);
$this->assertFalse($php_read->writeable());
}
/**
* @covers ::deleteAll
*/
public function testDeleteAll() {
$php = new FileStorage($this->standardSettings);
$name = $this->randomMachineName() . '/' . $this->randomMachineName() . '.php';
// Find a global that doesn't exist.
do {
$random = mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write our the file so we can test deleting.
$code = "<?php\n\$GLOBALS[$random] = TRUE;";
$this->assertTrue($php->save($name, $code));
$php_read = new FileReadOnlyStorage($this->readonlyStorage);
$this->assertFalse($php_read->deleteAll());
// Make sure directory exists prior to removal.
$this->assertTrue(file_exists($this->directory . '/test'), 'File storage directory does not exist.');
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\FileStorage;
/**
* @coversDefaultClass \Drupal\Component\PhpStorage\FileStorage
* @group Drupal
* @group PhpStorage
*/
class FileStorageTest extends PhpStorageTestBase {
/**
* Standard test settings to pass to storage instances.
*
* @var array
*/
protected $standardSettings;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->standardSettings = [
'directory' => $this->directory,
'bin' => 'test',
];
}
/**
* Tests basic load/save/delete operations.
*
* @covers ::load
* @covers ::save
* @covers ::exists
* @covers ::delete
*/
public function testCRUD() {
$php = new FileStorage($this->standardSettings);
$this->assertCRUD($php);
}
/**
* @covers ::writeable
*/
public function testWriteable() {
$php = new FileStorage($this->standardSettings);
$this->assertTrue($php->writeable());
}
/**
* @covers ::deleteAll
*/
public function testDeleteAll() {
// Write out some files.
$php = new FileStorage($this->standardSettings);
$name = $this->randomMachineName() . '/' . $this->randomMachineName() . '.php';
// Find a global that doesn't exist.
do {
$random = mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write out a PHP file and ensure it's successfully loaded.
$code = "<?php\n\$GLOBALS[$random] = TRUE;";
$this->assertTrue($php->save($name, $code), 'Saved php file');
$php->load($name);
$this->assertTrue($GLOBALS[$random], 'File saved correctly with correct value');
// Make sure directory exists prior to removal.
$this->assertTrue(file_exists($this->directory . '/test'), 'File storage directory does not exist.');
$this->assertTrue($php->deleteAll(), 'Delete all reported success');
$this->assertFalse($php->load($name));
$this->assertFalse(file_exists($this->directory . '/test'), 'File storage directory does not exist after call to deleteAll()');
// Should still return TRUE if directory has already been deleted.
$this->assertTrue($php->deleteAll(), 'Delete all succeeds with nothing to delete');
unset($GLOBALS[$random]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\Component\PhpStorage;
/**
* Tests the MTimeProtectedFastFileStorage implementation.
*
* @coversDefaultClass \Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage
*
* @group Drupal
* @group PhpStorage
*/
class MTimeProtectedFastFileStorageTest extends MTimeProtectedFileStorageBase {
/**
* The expected test results for the security test.
*
* The first iteration does not change the directory mtime so this class will
* include the hacked file on the first try but the second test will change
* the directory mtime and so on the second try the file will not be included.
*/
protected $expected = [TRUE, FALSE];
/**
* The PHP storage class to test.
*/
protected $storageClass = 'Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage';
}
@@ -0,0 +1,127 @@
<?php
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\Utility\Crypt;
/**
* Base test class for MTime protected storage.
*/
abstract class MTimeProtectedFileStorageBase extends PhpStorageTestBase {
/**
* The PHP storage class to test.
*
* This should be overridden by extending classes.
*/
protected $storageClass;
/**
* The secret string to use for file creation.
*
* @var string
*/
protected $secret;
/**
* Test settings to pass to storage instances.
*
* @var array
*/
protected $settings;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->secret = $this->randomMachineName();
$this->settings = [
'directory' => $this->directory,
'bin' => 'test',
'secret' => $this->secret,
];
}
/**
* Tests basic load/save/delete operations.
*
* @covers ::load
* @covers ::save
* @covers ::delete
* @covers ::exists
*/
public function testCRUD() {
$php = new $this->storageClass($this->settings);
$this->assertCRUD($php);
}
/**
* Tests the security of the MTimeProtectedFileStorage implementation.
*
* We test two attacks: first changes the file mtime, then the directory
* mtime too.
*
* We need to delay over 1 second for mtime test.
* @medium
*/
public function testSecurity() {
$php = new $this->storageClass($this->settings);
$name = 'simpletest.php';
$php->save($name, '<?php');
$expected_root_directory = $this->directory . '/test';
if (substr($name, -4) === '.php') {
$expected_directory = $expected_root_directory . '/' . substr($name, 0, -4);
}
else {
$expected_directory = $expected_root_directory . '/' . $name;
}
$directory_mtime = filemtime($expected_directory);
$expected_filename = $expected_directory . '/' . Crypt::hmacBase64($name, $this->secret . $directory_mtime) . '.php';
// Ensure the file exists and that it and the containing directory have
// minimal permissions. fileperms() can return high bits unrelated to
// permissions, so mask with 0777.
$this->assertTrue(file_exists($expected_filename));
$this->assertSame(fileperms($expected_filename) & 0777, 0444);
$this->assertSame(fileperms($expected_directory) & 0777, 0777);
// Ensure the root directory for the bin has a .htaccess file denying web
// access.
$this->assertSame(file_get_contents($expected_root_directory . '/.htaccess'), call_user_func([$this->storageClass, 'htaccessLines']));
// Ensure that if the file is replaced with an untrusted one (due to another
// script's file upload vulnerability), it does not get loaded. Since mtime
// granularity is 1 second, we cannot prevent an attack that happens within
// a second of the initial save().
sleep(1);
for ($i = 0; $i < 2; $i++) {
$php = new $this->storageClass($this->settings);
$GLOBALS['hacked'] = FALSE;
$untrusted_code = "<?php\n" . '$GLOBALS["hacked"] = TRUE;';
chmod($expected_directory, 0700);
chmod($expected_filename, 0700);
if ($i) {
// Now try to write the file in such a way that the directory mtime
// changes and invalidates the hash.
file_put_contents($expected_filename . '.tmp', $untrusted_code);
rename($expected_filename . '.tmp', $expected_filename);
}
else {
// On the first try do not change the directory mtime but the filemtime
// is now larger than the directory mtime.
file_put_contents($expected_filename, $untrusted_code);
}
chmod($expected_filename, 0400);
chmod($expected_directory, 0100);
$this->assertSame(file_get_contents($expected_filename), $untrusted_code);
$this->assertSame($php->exists($name), $this->expected[$i]);
$this->assertSame($php->load($name), $this->expected[$i]);
$this->assertSame($GLOBALS['hacked'], $this->expected[$i]);
}
unset($GLOBALS['hacked']);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Drupal\Tests\Component\PhpStorage;
/**
* Tests the MTimeProtectedFileStorage implementation.
*
* @coversDefaultClass \Drupal\Component\PhpStorage\MTimeProtectedFileStorage
*
* @group Drupal
* @group PhpStorage
*/
class MTimeProtectedFileStorageTest extends MTimeProtectedFileStorageBase {
/**
* The expected test results for the security test.
*
* The default implementation protects against even the filemtime change so
* both iterations will return FALSE.
*/
protected $expected = [FALSE, FALSE];
/**
* The PHP storage class to test.
*/
protected $storageClass = 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage';
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\PhpStorageInterface;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* Base test for PHP storages.
*/
abstract class PhpStorageTestBase extends UnitTestCase {
/**
* A unique per test class directory path to test php storage.
*
* @var string
*/
protected $directory;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
vfsStream::setup('exampleDir');
$this->directory = vfsStream::url('exampleDir');
}
/**
* Assert that a PHP storage's load/save/delete operations work.
*/
public function assertCRUD($php) {
$name = $this->randomMachineName() . '/' . $this->randomMachineName() . '.php';
// Find a global that doesn't exist.
do {
$random = mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write out a PHP file and ensure it's successfully loaded.
$code = "<?php\n\$GLOBALS[$random] = TRUE;";
$success = $php->save($name, $code);
$this->assertTrue($success, 'Saved php file');
$php->load($name);
$this->assertTrue($GLOBALS[$random], 'File saved correctly with correct value');
// Run additional asserts.
$this->additionalAssertCRUD($php, $name);
// If the file was successfully loaded, it must also exist, but ensure the
// exists() method returns that correctly.
$this->assertTrue($php->exists($name), 'Exists works correctly');
// Delete the file, and then ensure exists() returns FALSE.
$this->assertTrue($php->delete($name), 'Delete succeeded');
$this->assertFalse($php->exists($name), 'Delete deleted file');
// Ensure delete() can be called on a non-existing file. It should return
// FALSE, but not trigger errors.
$this->assertFalse($php->delete($name), 'Delete fails on missing file');
unset($GLOBALS[$random]);
}
/**
* Additional asserts to be run.
*
* @param \Drupal\Component\PhpStorage\PhpStorageInterface $php
* The PHP storage object.
* @param string $name
* The name of an object. It should exist in the storage.
*/
protected function additionalAssertCRUD(PhpStorageInterface $php, $name) {
// By default do not do any additional asserts. This is a way of extending
// tests in contrib.
}
}
@@ -0,0 +1,101 @@
<?php
namespace Drupal\Tests\Component\Plugin\Context;
use Drupal\Component\Plugin\Context\Context;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Context\Context
* @group Plugin
*/
class ContextTest extends UnitTestCase {
/**
* Data provider for testGetContextValue.
*/
public function providerGetContextValue() {
return [
['context_value', 'context_value', FALSE, 'data_type'],
[NULL, NULL, FALSE, 'data_type'],
['will throw exception', NULL, TRUE, 'data_type'],
];
}
/**
* @covers ::getContextValue
* @dataProvider providerGetContextValue
*/
public function testGetContextValue($expected, $context_value, $is_required, $data_type) {
// Mock a Context object.
$mock_context = $this->getMockBuilder('Drupal\Component\Plugin\Context\Context')
->disableOriginalConstructor()
->setMethods(['getContextDefinition'])
->getMock();
// If the context value exists, getContextValue() behaves like a normal
// getter.
if ($context_value) {
// Set visibility of contextValue.
$ref_context_value = new \ReflectionProperty($mock_context, 'contextValue');
$ref_context_value->setAccessible(TRUE);
// Set contextValue to a testable state.
$ref_context_value->setValue($mock_context, $context_value);
// Exercise getContextValue().
$this->assertEquals($context_value, $mock_context->getContextValue());
}
// If no context value exists, we have to cover either returning NULL or
// throwing an exception if the definition requires it.
else {
// Create a mock definition.
$mock_definition = $this->getMockBuilder('Drupal\Component\Plugin\Context\ContextDefinitionInterface')
->setMethods(['isRequired', 'getDataType'])
->getMockForAbstractClass();
// Set expectation for isRequired().
$mock_definition->expects($this->once())
->method('isRequired')
->willReturn($is_required);
// Set expectation for getDataType().
$mock_definition->expects($this->exactly(
$is_required ? 1 : 0
))
->method('getDataType')
->willReturn($data_type);
// Set expectation for getContextDefinition().
$mock_context->expects($this->once())
->method('getContextDefinition')
->willReturn($mock_definition);
// Set expectation for exception.
if ($is_required) {
$this->setExpectedException(
'Drupal\Component\Plugin\Exception\ContextException',
sprintf("The %s context is required and not present.", $data_type)
);
}
// Exercise getContextValue().
$this->assertEquals($context_value, $mock_context->getContextValue());
}
}
/**
* @covers ::getContextValue
*/
public function testDefaultValue() {
$mock_definition = $this->getMockBuilder('Drupal\Component\Plugin\Context\ContextDefinitionInterface')
->setMethods(['getDefaultValue'])
->getMockForAbstractClass();
$mock_definition->expects($this->once())
->method('getDefaultValue')
->willReturn('test');
$context = new Context($mock_definition);
$this->assertEquals('test', $context->getContextValue());
}
}
@@ -0,0 +1,147 @@
<?php
namespace Drupal\Tests\Component\Plugin;
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\plugin_test\Plugin\plugin_test\fruit\Cherry;
use Drupal\plugin_test\Plugin\plugin_test\fruit\FruitInterface;
use Drupal\plugin_test\Plugin\plugin_test\fruit\Kale;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Factory\DefaultFactory
* @group Plugin
*/
class DefaultFactoryTest extends UnitTestCase {
/**
* Tests getPluginClass() with a valid array plugin definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithValidArrayPluginDefinition() {
$plugin_class = Cherry::class;
$class = DefaultFactory::getPluginClass('cherry', ['class' => $plugin_class]);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a valid object plugin definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithValidObjectPluginDefinition() {
$plugin_class = Cherry::class;
$plugin_definition = $this->getMock(PluginDefinitionInterface::class);
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$class = DefaultFactory::getPluginClass('cherry', $plugin_definition);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a missing class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithMissingClassWithArrayPluginDefinition() {
$this->setExpectedException(PluginException::class, 'The plugin (cherry) did not specify an instance class.');
DefaultFactory::getPluginClass('cherry', []);
}
/**
* Tests getPluginClass() with a missing class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithMissingClassWithObjectPluginDefinition() {
$plugin_definition = $this->getMock(PluginDefinitionInterface::class);
$this->setExpectedException(PluginException::class, 'The plugin (cherry) did not specify an instance class.');
DefaultFactory::getPluginClass('cherry', $plugin_definition);
}
/**
* Tests getPluginClass() with a not existing class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithNotExistingClassWithArrayPluginDefinition() {
$this->setExpectedException(PluginException::class, 'Plugin (kiwifruit) instance class "\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit" does not exist.');
DefaultFactory::getPluginClass('kiwifruit', ['class' => '\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit']);
}
/**
* Tests getPluginClass() with a not existing class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithNotExistingClassWithObjectPluginDefinition() {
$plugin_class = '\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit';
$plugin_definition = $this->getMock(PluginDefinitionInterface::class);
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$this->setExpectedException(PluginException::class);
DefaultFactory::getPluginClass('kiwifruit', $plugin_definition);
}
/**
* Tests getPluginClass() with a required interface.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceWithArrayPluginDefinition() {
$plugin_class = Cherry::class;
$class = DefaultFactory::getPluginClass('cherry', ['class' => $plugin_class], FruitInterface::class);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a required interface.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceWithObjectPluginDefinition() {
$plugin_class = Cherry::class;
$plugin_definition = $this->getMock(PluginDefinitionInterface::class);
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$class = DefaultFactory::getPluginClass('cherry', $plugin_definition, FruitInterface::class);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a required interface but no implementation.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceAndInvalidClassWithArrayPluginDefinition() {
$plugin_class = Kale::class;
$this->setExpectedException(PluginException::class, 'Plugin "cherry" (Drupal\plugin_test\Plugin\plugin_test\fruit\Kale) must implement interface Drupal\plugin_test\Plugin\plugin_test\fruit\FruitInterface.');
DefaultFactory::getPluginClass('cherry', ['class' => $plugin_class, 'provider' => 'core'], FruitInterface::class);
}
/**
* Tests getPluginClass() with a required interface but no implementation.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceAndInvalidClassWithObjectPluginDefinition() {
$plugin_class = Kale::class;
$plugin_definition = $this->getMock(PluginDefinitionInterface::class);
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$this->setExpectedException(PluginException::class);
DefaultFactory::getPluginClass('cherry', $plugin_definition, FruitInterface::class);
}
}
@@ -0,0 +1,65 @@
<?php
namespace Drupal\Tests\Component\Plugin\Discovery;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait
* @uses \Drupal\Component\Plugin\Discovery\DiscoveryTrait
* @group Plugin
*/
class DiscoveryCachedTraitTest extends UnitTestCase {
/**
* Data provider for testGetDefinition().
*
* @return array
* - Expected result from getDefinition().
* - Cached definitions to be placed into self::$definitions
* - Definitions to be returned by getDefinitions().
* - Plugin name to query for.
*/
public function providerGetDefinition() {
return [
['definition', [], ['plugin_name' => 'definition'], 'plugin_name'],
['definition', ['plugin_name' => 'definition'], [], 'plugin_name'],
[NULL, ['plugin_name' => 'definition'], [], 'bad_plugin_name'],
];
}
/**
* @covers ::getDefinition
* @dataProvider providerGetDefinition
*/
public function testGetDefinition($expected, $cached_definitions, $get_definitions, $plugin_id) {
// Mock a DiscoveryCachedTrait.
$trait = $this->getMockForTrait('Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait');
$reflection_definitions = new \ReflectionProperty($trait, 'definitions');
$reflection_definitions->setAccessible(TRUE);
// getDefinition() needs the ::$definitions property to be set in one of two
// ways: 1) As existing cached data, or 2) as a side-effect of calling
// getDefinitions().
// If there are no cached definitions, then we have to fake the side-effect
// of getDefinitions().
if (count($cached_definitions) < 1) {
$trait->expects($this->once())
->method('getDefinitions')
// Use a callback method, so we can perform the side-effects.
->willReturnCallback(function() use ($reflection_definitions, $trait, $get_definitions) {
$reflection_definitions->setValue($trait, $get_definitions);
return $get_definitions;
});
}
else {
// Put $cached_definitions into our mocked ::$definitions.
$reflection_definitions->setValue($trait, $cached_definitions);
}
// Call getDefinition(), with $exception_on_invalid always FALSE.
$this->assertSame(
$expected,
$trait->getDefinition($plugin_id, FALSE)
);
}
}
@@ -0,0 +1,150 @@
<?php
namespace Drupal\Tests\Component\Plugin\Discovery;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Tests\UnitTestCase;
/**
* @group Plugin
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\DiscoveryTrait
*/
class DiscoveryTraitTest extends UnitTestCase {
/**
* Data provider for testDoGetDefinition().
*
* @return array
* - Expected plugin definition.
* - Plugin definition array, to pass to doGetDefinition().
* - Plugin ID to get, passed to doGetDefinition().
*/
public function providerDoGetDefinition() {
return [
['definition', ['plugin_name' => 'definition'], 'plugin_name'],
[NULL, ['plugin_name' => 'definition'], 'bad_plugin_name'],
];
}
/**
* @covers ::doGetDefinition
* @dataProvider providerDoGetDefinition
*/
public function testDoGetDefinition($expected, $definitions, $plugin_id) {
// Mock the trait.
$trait = $this->getMockForTrait('Drupal\Component\Plugin\Discovery\DiscoveryTrait');
// Un-protect the method using reflection.
$method_ref = new \ReflectionMethod($trait, 'doGetDefinition');
$method_ref->setAccessible(TRUE);
// Call doGetDefinition, with $exception_on_invalid always FALSE.
$this->assertSame(
$expected,
$method_ref->invoke($trait, $definitions, $plugin_id, FALSE)
);
}
/**
* Data provider for testDoGetDefinitionException()
*
* @return array
* - Expected plugin definition.
* - Plugin definition array, to pass to doGetDefinition().
* - Plugin ID to get, passed to doGetDefinition().
*/
public function providerDoGetDefinitionException() {
return [
[FALSE, ['plugin_name' => 'definition'], 'bad_plugin_name'],
];
}
/**
* @covers ::doGetDefinition
* @dataProvider providerDoGetDefinitionException
* @uses \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function testDoGetDefinitionException($expected, $definitions, $plugin_id) {
// Mock the trait.
$trait = $this->getMockForTrait('Drupal\Component\Plugin\Discovery\DiscoveryTrait');
// Un-protect the method using reflection.
$method_ref = new \ReflectionMethod($trait, 'doGetDefinition');
$method_ref->setAccessible(TRUE);
// Call doGetDefinition, with $exception_on_invalid always TRUE.
$this->setExpectedException(PluginNotFoundException::class);
$method_ref->invoke($trait, $definitions, $plugin_id, TRUE);
}
/**
* @covers ::getDefinition
* @dataProvider providerDoGetDefinition
*/
public function testGetDefinition($expected, $definitions, $plugin_id) {
// Since getDefinition is a wrapper around doGetDefinition(), we can re-use
// its data provider. We just have to tell abstract method getDefinitions()
// to use the $definitions array.
$trait = $this->getMockForTrait('Drupal\Component\Plugin\Discovery\DiscoveryTrait');
$trait->expects($this->once())
->method('getDefinitions')
->willReturn($definitions);
// Call getDefinition(), with $exception_on_invalid always FALSE.
$this->assertSame(
$expected,
$trait->getDefinition($plugin_id, FALSE)
);
}
/**
* @covers ::getDefinition
* @dataProvider providerDoGetDefinitionException
* @uses \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function testGetDefinitionException($expected, $definitions, $plugin_id) {
// Since getDefinition is a wrapper around doGetDefinition(), we can re-use
// its data provider. We just have to tell abstract method getDefinitions()
// to use the $definitions array.
$trait = $this->getMockForTrait('Drupal\Component\Plugin\Discovery\DiscoveryTrait');
$trait->expects($this->once())
->method('getDefinitions')
->willReturn($definitions);
// Call getDefinition(), with $exception_on_invalid always TRUE.
$this->setExpectedException(PluginNotFoundException::class);
$trait->getDefinition($plugin_id, TRUE);
}
/**
* Data provider for testHasDefinition().
*
* @return array
* - Expected TRUE or FALSE.
* - Plugin ID to look for.
*/
public function providerHasDefinition() {
return [
[TRUE, 'valid'],
[FALSE, 'not_valid'],
];
}
/**
* @covers ::hasDefinition
* @dataProvider providerHasDefinition
*/
public function testHasDefinition($expected, $plugin_id) {
$trait = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\DiscoveryTrait')
->setMethods(['getDefinition'])
->getMockForTrait();
// Set up our mocked getDefinition() to return TRUE for 'valid' and FALSE
// for 'not_valid'.
$trait->expects($this->once())
->method('getDefinition')
->will($this->returnValueMap([
['valid', FALSE, TRUE],
['not_valid', FALSE, FALSE],
]));
// Call hasDefinition().
$this->assertSame(
$expected,
$trait->hasDefinition($plugin_id)
);
}
}
@@ -0,0 +1,229 @@
<?php
namespace Drupal\Tests\Component\Plugin\Discovery;
use Drupal\Tests\UnitTestCase;
/**
* @group Plugin
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator
*/
class StaticDiscoveryDecoratorTest extends UnitTestCase {
/**
* Helper method to provide a mocked callback object with expectations.
*
* If there should be a registered definition, then we have to place a
* \Callable in the mock object. The return value of this callback is
* never used.
*
* @return \PHPUnit_Framework_MockObject_MockObject
* Mocked object with expectation of registerDefinitionsCallback() being
* called once.
*/
public function getRegisterDefinitionsCallback() {
$mock_callable = $this->getMockBuilder('\stdClass')
->setMethods(['registerDefinitionsCallback'])
->getMock();
// Set expectations for the callback method.
$mock_callable->expects($this->once())
->method('registerDefinitionsCallback');
return $mock_callable;
}
/**
* Data provider for testGetDefinitions().
*
* @return array
* - Expected plugin definition.
* - Whether we require the method to register definitions through a
* callback.
* - Whether to throw an exception if the definition is invalid.
* - A plugin definition.
* - Base plugin ID.
*/
public function providerGetDefinition() {
return [
['is_defined', TRUE, FALSE, ['plugin-definition' => 'is_defined'], 'plugin-definition'],
// Make sure we don't call the decorated method if we shouldn't.
['is_defined', FALSE, FALSE, ['plugin-definition' => 'is_defined'], 'plugin-definition'],
// Return NULL for bad plugin id.
[NULL, FALSE, FALSE, ['plugin-definition' => 'is_defined'], 'BAD-plugin-definition'],
// Generate an exception.
[NULL, FALSE, TRUE, ['plugin-definition' => 'is_defined'], 'BAD-plugin-definition'],
];
}
/**
* @covers ::getDefinition
* @dataProvider providerGetDefinition
*/
public function testGetDefinition($expected, $has_register_definitions, $exception_on_invalid, $definitions, $base_plugin_id) {
// Mock our StaticDiscoveryDecorator.
$mock_decorator = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator')
->disableOriginalConstructor()
->setMethods(['registeredDefintionCallback'])
->getMock();
// Set up the ::$registerDefinitions property.
$ref_register_definitions = new \ReflectionProperty($mock_decorator, 'registerDefinitions');
$ref_register_definitions->setAccessible(TRUE);
if ($has_register_definitions) {
// Set the callback object on the mocked decorator.
$ref_register_definitions->setValue(
$mock_decorator,
[$this->getRegisterDefinitionsCallback(), 'registerDefinitionsCallback']
);
}
else {
// There should be no registerDefinitions callback.
$ref_register_definitions->setValue($mock_decorator, NULL);
}
// Set up ::$definitions to an empty array.
$ref_definitions = new \ReflectionProperty($mock_decorator, 'definitions');
$ref_definitions->setAccessible(TRUE);
$ref_definitions->setValue($mock_decorator, []);
// Mock a decorated object.
$mock_decorated = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\DiscoveryInterface')
->setMethods(['getDefinitions'])
->getMockForAbstractClass();
// Return our definitions from getDefinitions().
$mock_decorated->expects($this->once())
->method('getDefinitions')
->willReturn($definitions);
// Set up ::$decorated to our mocked decorated object.
$ref_decorated = new \ReflectionProperty($mock_decorator, 'decorated');
$ref_decorated->setAccessible(TRUE);
$ref_decorated->setValue($mock_decorator, $mock_decorated);
if ($exception_on_invalid) {
$this->setExpectedException('Drupal\Component\Plugin\Exception\PluginNotFoundException');
}
// Exercise getDefinition(). It calls parent::getDefinition().
$this->assertEquals(
$expected,
$mock_decorator->getDefinition($base_plugin_id, $exception_on_invalid)
);
}
/**
* Data provider for testGetDefinitions().
*
* @return array
* - bool Whether the test mock has a callback.
* - array Plugin definitions.
*/
public function providerGetDefinitions() {
return [
[TRUE, ['definition' => 'is_fake']],
[FALSE, ['definition' => 'array_of_stuff']],
];
}
/**
* @covers ::getDefinitions
* @dataProvider providerGetDefinitions
*/
public function testGetDefinitions($has_register_definitions, $definitions) {
// Mock our StaticDiscoveryDecorator.
$mock_decorator = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator')
->disableOriginalConstructor()
->setMethods(['registeredDefintionCallback'])
->getMock();
// Set up the ::$registerDefinitions property.
$ref_register_definitions = new \ReflectionProperty($mock_decorator, 'registerDefinitions');
$ref_register_definitions->setAccessible(TRUE);
if ($has_register_definitions) {
// Set the callback object on the mocked decorator.
$ref_register_definitions->setValue(
$mock_decorator,
[$this->getRegisterDefinitionsCallback(), 'registerDefinitionsCallback']
);
}
else {
// There should be no registerDefinitions callback.
$ref_register_definitions->setValue($mock_decorator, NULL);
}
// Set up ::$definitions to an empty array.
$ref_definitions = new \ReflectionProperty($mock_decorator, 'definitions');
$ref_definitions->setAccessible(TRUE);
$ref_definitions->setValue($mock_decorator, []);
// Mock a decorated object.
$mock_decorated = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\DiscoveryInterface')
->setMethods(['getDefinitions'])
->getMockForAbstractClass();
// Our mocked method will return any arguments sent to it.
$mock_decorated->expects($this->once())
->method('getDefinitions')
->willReturn($definitions);
// Set up ::$decorated to our mocked decorated object.
$ref_decorated = new \ReflectionProperty($mock_decorator, 'decorated');
$ref_decorated->setAccessible(TRUE);
$ref_decorated->setValue($mock_decorator, $mock_decorated);
// Exercise getDefinitions(). It calls parent::getDefinitions() but in this
// case there will be no side-effects.
$this->assertArrayEquals(
$definitions,
$mock_decorator->getDefinitions()
);
}
/**
* Data provider for testCall().
*
* @return array
* - Method name.
* - Array of arguments to pass to the method, with the expectation that our
* mocked __call() will return them.
*/
public function providerCall() {
return [
['complexArguments', ['1', 2.0, 3, ['4' => 'five']]],
['noArguments', []],
];
}
/**
* @covers ::__call
* @dataProvider providerCall
*/
public function testCall($method, $args) {
// Mock a decorated object.
$mock_decorated = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\DiscoveryInterface')
->setMethods([$method])
->getMockForAbstractClass();
// Our mocked method will return any arguments sent to it.
$mock_decorated->expects($this->once())
->method($method)
->willReturnCallback(
function () {
return \func_get_args();
}
);
// Create a mock decorator.
$mock_decorator = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator')
->disableOriginalConstructor()
->getMock();
// Poke the decorated object into our decorator.
$ref_decorated = new \ReflectionProperty($mock_decorator, 'decorated');
$ref_decorated->setAccessible(TRUE);
$ref_decorated->setValue($mock_decorator, $mock_decorated);
// Exercise __call.
$this->assertArrayEquals(
$args,
\call_user_func_array([$mock_decorated, $method], $args)
);
}
}
@@ -0,0 +1,216 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Plugin\Factory\ReflectionFactoryTest.
*
* Also contains Argument* classes used as data for testing.
*/
namespace Drupal\Tests\Component\Plugin\Factory;
use Drupal\Component\Plugin\Factory\ReflectionFactory;
use Drupal\Tests\UnitTestCase;
/**
* @group Plugin
* @coversDefaultClass \Drupal\Component\Plugin\Factory\ReflectionFactory
*/
class ReflectionFactoryTest extends UnitTestCase {
/**
* Data provider for testGetInstanceArguments.
*
* The classes used here are defined at the bottom of this file.
*
* @return array
* - Expected output.
* - Class to reflect for input to getInstanceArguments().
* - $plugin_id parameter to getInstanceArguments().
* - $plugin_definition parameter to getInstanceArguments().
* - $configuration parameter to getInstanceArguments().
*/
public function providerGetInstanceArguments() {
return [
[
['arguments_plugin_id'],
'Drupal\Tests\Component\Plugin\Factory\ArgumentsPluginId',
'arguments_plugin_id',
['arguments_plugin_id' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsPluginId']],
[],
],
[
[[], ['arguments_many' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsMany']], 'arguments_many', 'default_value', 'what_default'],
'Drupal\Tests\Component\Plugin\Factory\ArgumentsMany',
'arguments_many',
['arguments_many' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsMany']],
[],
],
[
// Config array key exists and is set.
['thing'],
'Drupal\Tests\Component\Plugin\Factory\ArgumentsConfigArrayKey',
'arguments_config_array_key',
['arguments_config_array_key' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsConfigArrayKey']],
['config_name' => 'thing'],
],
[
// Config array key exists and is not set.
[NULL],
'Drupal\Tests\Component\Plugin\Factory\ArgumentsConfigArrayKey',
'arguments_config_array_key',
['arguments_config_array_key' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsConfigArrayKey']],
['config_name' => NULL],
],
[
// Touch the else clause at the end of the method.
[NULL, NULL, NULL, NULL],
'Drupal\Tests\Component\Plugin\Factory\ArgumentsAllNull',
'arguments_all_null',
['arguments_all_null' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsAllNull']],
[],
],
[
// A plugin with no constructor.
[NULL, NULL, NULL, NULL],
'Drupal\Tests\Component\Plugin\Factory\ArgumentsNoConstructor',
'arguments_no_constructor',
['arguments_no_constructor' => ['class' => 'Drupal\Tests\Component\Plugin\Factory\ArgumentsNoConstructor']],
[],
],
];
}
/**
* @covers ::createInstance
* @dataProvider providerGetInstanceArguments
*/
public function testCreateInstance($expected, $reflector_name, $plugin_id, $plugin_definition, $configuration) {
// Create a mock DiscoveryInterface which can return our plugin definition.
$mock_discovery = $this->getMockBuilder('Drupal\Component\Plugin\Discovery\DiscoveryInterface')
->setMethods(['getDefinition', 'getDefinitions', 'hasDefinition'])
->getMock();
$mock_discovery->expects($this->never())->method('getDefinitions');
$mock_discovery->expects($this->never())->method('hasDefinition');
$mock_discovery->expects($this->once())
->method('getDefinition')
->willReturn($plugin_definition);
// Create a stub ReflectionFactory object. We use StubReflectionFactory
// because createInstance() has a dependency on a static method.
// StubReflectionFactory overrides this static method.
$reflection_factory = new StubReflectionFactory($mock_discovery);
// Finally test that createInstance() returns an object of the class we
// want.
$this->assertInstanceOf($reflector_name, $reflection_factory->createInstance($plugin_id));
}
/**
* @covers ::getInstanceArguments
* @dataProvider providerGetInstanceArguments
*/
public function testGetInstanceArguments($expected, $reflector_name, $plugin_id, $plugin_definition, $configuration) {
$reflection_factory = $this->getMockBuilder('Drupal\Component\Plugin\Factory\ReflectionFactory')
->disableOriginalConstructor()
->getMock();
$get_instance_arguments_ref = new \ReflectionMethod($reflection_factory, 'getInstanceArguments');
$get_instance_arguments_ref->setAccessible(TRUE);
// Special case for plugin class without a constructor.
// getInstanceArguments() throws an exception if there's no constructor.
// This is not a documented behavior of getInstanceArguments(), but allows
// us to use one data set for this test method as well as
// testCreateInstance().
if ($plugin_id == 'arguments_no_constructor') {
$this->setExpectedException('\ReflectionException');
}
// Finally invoke getInstanceArguments() on our mocked factory.
$ref = new \ReflectionClass($reflector_name);
$result = $get_instance_arguments_ref->invoke(
$reflection_factory, $ref, $plugin_id, $plugin_definition, $configuration);
$this->assertEquals($expected, $result);
}
}
/**
* Override ReflectionFactory because ::createInstance() calls a static method.
*
* We have to override getPluginClass so that we can stub out its return value.
*/
class StubReflectionFactory extends ReflectionFactory {
/**
* {@inheritdoc}
*/
public static function getPluginClass($plugin_id, $plugin_definition = NULL, $required_interface = NULL) {
// Return the class name from the plugin definition.
return $plugin_definition[$plugin_id]['class'];
}
}
/**
* A stub class used by testGetInstanceArguments().
*
* @see providerGetInstanceArguments()
*/
class ArgumentsPluginId {
public function __construct($plugin_id) {
// No-op.
}
}
/**
* A stub class used by testGetInstanceArguments().
*
* @see providerGetInstanceArguments()
*/
class ArgumentsMany {
public function __construct(
$configuration, $plugin_definition, $plugin_id, $foo = 'default_value', $what_am_i_doing_here = 'what_default'
) {
// No-op.
}
}
/**
* A stub class used by testGetInstanceArguments().
*
* @see providerGetInstanceArguments()
*/
class ArgumentsConfigArrayKey {
public function __construct($config_name) {
// No-op.
}
}
/**
* A stub class used by testGetInstanceArguments().
*
* @see providerGetInstanceArguments()
*/
class ArgumentsAllNull {
public function __construct($charismatic, $demure, $delightful, $electrostatic) {
// No-op.
}
}
/**
* A stub class used by testGetInstanceArguments().
*
* @see providerGetInstanceArguments()
*/
class ArgumentsNoConstructor {
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\Tests\Component\Plugin;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\PluginBase
* @group Plugin
*/
class PluginBaseTest extends UnitTestCase {
/**
* @dataProvider providerTestGetPluginId
* @covers ::getPluginId
*/
public function testGetPluginId($plugin_id, $expected) {
$plugin_base = $this->getMockForAbstractClass('Drupal\Component\Plugin\PluginBase', [
[],
$plugin_id,
[],
]);
$this->assertEquals($expected, $plugin_base->getPluginId());
}
/**
* Returns test data for testGetPluginId().
*
* @return array
*/
public function providerTestGetPluginId() {
return [
['base_id', 'base_id'],
['base_id:derivative', 'base_id:derivative'],
];
}
/**
* @dataProvider providerTestGetBaseId
* @coves ::getBaseId
*/
public function testGetBaseId($plugin_id, $expected) {
/** @var \Drupal\Component\Plugin\PluginBase|\PHPUnit_Framework_MockObject_MockObject $plugin_base */
$plugin_base = $this->getMockForAbstractClass('Drupal\Component\Plugin\PluginBase', [
[],
$plugin_id,
[],
]);
$this->assertEquals($expected, $plugin_base->getBaseId());
}
/**
* Returns test data for testGetBaseId().
*
* @return array
*/
public function providerTestGetBaseId() {
return [
['base_id', 'base_id'],
['base_id:derivative', 'base_id'],
];
}
/**
* @dataProvider providerTestGetDerivativeId
* @covers ::getDerivativeId
*/
public function testGetDerivativeId($plugin_id = NULL, $expected = NULL) {
/** @var \Drupal\Component\Plugin\PluginBase|\PHPUnit_Framework_MockObject_MockObject $plugin_base */
$plugin_base = $this->getMockForAbstractClass('Drupal\Component\Plugin\PluginBase', [
[],
$plugin_id,
[],
]);
$this->assertEquals($expected, $plugin_base->getDerivativeId());
}
/**
* Returns test data for testGetDerivativeId().
*
* @return array
*/
public function providerTestGetDerivativeId() {
return [
['base_id', NULL],
['base_id:derivative', 'derivative'],
];
}
/**
* @covers ::getPluginDefinition
*/
public function testGetPluginDefinition() {
$plugin_base = $this->getMockForAbstractClass('Drupal\Component\Plugin\PluginBase', [
[],
'plugin_id',
['value', ['key' => 'value']],
]);
$this->assertEquals(['value', ['key' => 'value']], $plugin_base->getPluginDefinition());
}
}
@@ -0,0 +1,93 @@
<?php
namespace Drupal\Tests\Component\Plugin;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\PluginManagerBase
* @group Plugin
*/
class PluginManagerBaseTest extends UnitTestCase {
/**
* A callback method for mocking FactoryInterface objects.
*/
public function createInstanceCallback() {
$args = func_get_args();
$plugin_id = $args[0];
$configuration = $args[1];
if ('invalid' == $plugin_id) {
throw new PluginNotFoundException($plugin_id);
}
return [
'plugin_id' => $plugin_id,
'configuration' => $configuration,
];
}
/**
* Generates a mocked FactoryInterface object with known properties.
*/
public function getMockFactoryInterface($expects_count) {
$mock_factory = $this->getMockBuilder('Drupal\Component\Plugin\Factory\FactoryInterface')
->setMethods(['createInstance'])
->getMockForAbstractClass();
$mock_factory->expects($this->exactly($expects_count))
->method('createInstance')
->willReturnCallback([$this, 'createInstanceCallback']);
return $mock_factory;
}
/**
* Tests createInstance() with no fallback methods.
*
* @covers ::createInstance
*/
public function testCreateInstance() {
$manager = $this->getMockBuilder('Drupal\Component\Plugin\PluginManagerBase')
->getMockForAbstractClass();
// PluginManagerBase::createInstance() looks for a factory object and then
// calls createInstance() on it. So we have to mock a factory object.
$factory_ref = new \ReflectionProperty($manager, 'factory');
$factory_ref->setAccessible(TRUE);
$factory_ref->setValue($manager, $this->getMockFactoryInterface(1));
// Finally the test.
$configuration_array = ['config' => 'something'];
$result = $manager->createInstance('valid', $configuration_array);
$this->assertEquals('valid', $result['plugin_id']);
$this->assertArrayEquals($configuration_array, $result['configuration']);
}
/**
* Tests createInstance() with a fallback method.
*
* @covers ::createInstance
*/
public function testCreateInstanceFallback() {
// We use our special stub class which extends PluginManagerBase and also
// implements FallbackPluginManagerInterface.
$manager = new StubFallbackPluginManager();
// Put our stubbed factory on the base object.
$factory_ref = new \ReflectionProperty($manager, 'factory');
$factory_ref->setAccessible(TRUE);
// Set up the configuration array.
$configuration_array = ['config' => 'something'];
// Test with fallback interface and valid plugin_id.
$factory_ref->setValue($manager, $this->getMockFactoryInterface(1));
$no_fallback_result = $manager->createInstance('valid', $configuration_array);
$this->assertEquals('valid', $no_fallback_result['plugin_id']);
$this->assertArrayEquals($configuration_array, $no_fallback_result['configuration']);
// Test with fallback interface and invalid plugin_id.
$factory_ref->setValue($manager, $this->getMockFactoryInterface(2));
$fallback_result = $manager->createInstance('invalid', $configuration_array);
$this->assertEquals('invalid_fallback', $fallback_result['plugin_id']);
$this->assertArrayEquals($configuration_array, $fallback_result['configuration']);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Drupal\Tests\Component\Plugin;
use Drupal\Component\Plugin\FallbackPluginManagerInterface;
use Drupal\Component\Plugin\PluginManagerBase;
/**
* Stubs \Drupal\Component\Plugin\FallbackPluginManagerInterface.
*
* We have to stub \Drupal\Component\Plugin\FallbackPluginManagerInterface for
* \Drupal\Tests\Component\Plugin\PluginManagerBaseTest so that we can
* implement ::getFallbackPluginId().
*
* We do this so we can have it just return the plugin ID passed to it, with
* '_fallback' appended.
*/
class StubFallbackPluginManager extends PluginManagerBase implements FallbackPluginManagerInterface {
/**
* {@inheritdoc}
*/
public function getFallbackPluginId($plugin_id, array $configuration = []) {
// Minimally implement getFallbackPluginId so that we can test it.
return $plugin_id . '_fallback';
}
}
@@ -0,0 +1,445 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\ProxyBuilder\ProxyBuilderTest.
*/
namespace Drupal\Tests\Component\ProxyBuilder;
use Drupal\Component\ProxyBuilder\ProxyBuilder;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\ProxyBuilder\ProxyBuilder
* @group proxy_builder
*/
class ProxyBuilderTest extends UnitTestCase {
/**
* The tested proxy builder.
*
* @var \Drupal\Component\ProxyBuilder\ProxyBuilder
*/
protected $proxyBuilder;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->proxyBuilder = new ProxyBuilder();
}
/**
* @covers ::buildProxyClassName
*/
public function testBuildProxyClassName() {
$class_name = $this->proxyBuilder->buildProxyClassName('Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod');
$this->assertEquals('Drupal\Tests\ProxyClass\Component\ProxyBuilder\TestServiceNoMethod', $class_name);
}
/**
* @covers ::buildProxyClassName
*/
public function testBuildProxyClassNameForModule() {
$class_name = $this->proxyBuilder->buildProxyClassName('Drupal\views_ui\ParamConverter\ViewUIConverter');
$this->assertEquals('Drupal\views_ui\ProxyClass\ParamConverter\ViewUIConverter', $class_name);
}
/**
* @covers ::buildProxyNamespace
*/
public function testBuildProxyNamespace() {
$class_name = $this->proxyBuilder->buildProxyNamespace('Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod');
$this->assertEquals('Drupal\Tests\ProxyClass\Component\ProxyBuilder', $class_name);
}
/**
* Tests the basic methods like the constructor and the lazyLoadItself method.
*
* @covers ::build
* @covers ::buildConstructorMethod
* @covers ::buildLazyLoadItselfMethod
*/
public function testBuildNoMethod() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod';
$result = $this->proxyBuilder->build($class);
$this->assertEquals($this->buildExpectedClass($class, ''), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildMethodBody
*/
public function testBuildSimpleMethod() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceSimpleMethod';
$result = $this->proxyBuilder->build($class);
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public function method()
{
return $this->lazyLoadItself()->method();
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildParameter
* @covers ::buildMethodBody
*/
public function testBuildMethodWithParameter() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceMethodWithParameter';
$result = $this->proxyBuilder->build($class);
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public function methodWithParameter($parameter)
{
return $this->lazyLoadItself()->methodWithParameter($parameter);
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildParameter
* @covers ::buildMethodBody
*/
public function testBuildComplexMethod() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceComplexMethod';
$result = $this->proxyBuilder->build($class);
// @todo Solve the silly linebreak for array()
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public function complexMethod($parameter, callable $function, \Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod $test_service = NULL, array &$elements = array (
))
{
return $this->lazyLoadItself()->complexMethod($parameter, $function, $test_service, $elements);
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildMethodBody
*/
public function testBuildReturnReference() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceReturnReference';
$result = $this->proxyBuilder->build($class);
// @todo Solve the silly linebreak for array()
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public function &returnReference()
{
return $this->lazyLoadItself()->returnReference();
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildParameter
* @covers ::buildMethodBody
*/
public function testBuildWithInterface() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithInterface';
$result = $this->proxyBuilder->build($class);
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public function testMethod($parameter)
{
return $this->lazyLoadItself()->testMethod($parameter);
}
EOS;
$interface_string = ' implements \Drupal\Tests\Component\ProxyBuilder\TestInterface';
$this->assertEquals($this->buildExpectedClass($class, $method_body, $interface_string), $result);
}
/**
* @covers ::build
*/
public function testBuildWithNestedInterface() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithChildInterfaces';
$result = $this->proxyBuilder->build($class);
$method_body = '';
$interface_string = ' implements \Drupal\Tests\Component\ProxyBuilder\TestChildInterface';
$this->assertEquals($this->buildExpectedClass($class, $method_body, $interface_string), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildParameter
* @covers ::buildMethodBody
*/
public function testBuildWithProtectedAndPrivateMethod() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithProtectedMethods';
$result = $this->proxyBuilder->build($class);
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public function testMethod($parameter)
{
return $this->lazyLoadItself()->testMethod($parameter);
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod
* @covers ::buildParameter
* @covers ::buildMethodBody
*/
public function testBuildWithPublicStaticMethod() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithPublicStaticMethod';
$result = $this->proxyBuilder->build($class);
// Ensure that the static method is not wrapped.
$method_body = <<<'EOS'
/**
* {@inheritdoc}
*/
public static function testMethod($parameter)
{
\Drupal\Tests\Component\ProxyBuilder\TestServiceWithPublicStaticMethod::testMethod($parameter);
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* Constructs the expected class output.
*
* @param string $expected_methods_body
* The expected body of decorated methods.
*
* @return string
* The code of the entire proxy.
*/
protected function buildExpectedClass($class, $expected_methods_body, $interface_string = '') {
$namespace = ProxyBuilder::buildProxyNamespace($class);
$reflection = new \ReflectionClass($class);
$proxy_class = $reflection->getShortName();
$expected_string = <<<'EOS'
namespace {{ namespace }} {
/**
* Provides a proxy class for \{{ class }}.
*
* @see \Drupal\Component\ProxyBuilder
*/
class {{ proxy_class }}{{ interface_string }}
{
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \{{ class }}
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
{{ expected_methods_body }}
}
}
EOS;
$expected_methods_body = implode("\n", array_map(function ($value) {
if ($value === '') {
return $value;
}
return " $value";
}, explode("\n", $expected_methods_body)));
$expected_string = str_replace('{{ proxy_class }}', $proxy_class, $expected_string);
$expected_string = str_replace('{{ namespace }}', $namespace, $expected_string);
$expected_string = str_replace('{{ class }}', $class, $expected_string);
$expected_string = str_replace('{{ expected_methods_body }}', $expected_methods_body, $expected_string);
$expected_string = str_replace('{{ interface_string }}', $interface_string, $expected_string);
return $expected_string;
}
}
class TestServiceNoMethod {
}
class TestServiceSimpleMethod {
public function method() {
}
}
class TestServiceMethodWithParameter {
public function methodWithParameter($parameter) {
}
}
class TestServiceComplexMethod {
public function complexMethod($parameter, callable $function, TestServiceNoMethod $test_service = NULL, array &$elements = []) {
}
}
class TestServiceReturnReference {
public function &returnReference() {
}
}
interface TestInterface {
public function testMethod($parameter);
}
class TestServiceWithInterface implements TestInterface {
public function testMethod($parameter) {
}
}
class TestServiceWithProtectedMethods {
public function testMethod($parameter) {
}
protected function protectedMethod($parameter) {
}
protected function privateMethod($parameter) {
}
}
class TestServiceWithPublicStaticMethod {
public static function testMethod($parameter) {
}
}
interface TestBaseInterface {
}
interface TestChildInterface extends TestBaseInterface {
}
class TestServiceWithChildInterfaces implements TestChildInterface {
}
@@ -0,0 +1,102 @@
<?php
namespace Drupal\Tests\Component\Render;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Tests\UnitTestCase;
/**
* Tests the TranslatableMarkup class.
*
* @coversDefaultClass \Drupal\Component\Render\FormattableMarkup
* @group utility
*/
class FormattableMarkupTest extends UnitTestCase {
/**
* The error message of the last error in the error handler.
*
* @var string
*/
protected $lastErrorMessage;
/**
* The error number of the last error in the error handler.
*
* @var int
*/
protected $lastErrorNumber;
/**
* @covers ::__toString
* @covers ::jsonSerialize
*/
public function testToString() {
$string = 'Can I please have a @replacement';
$formattable_string = new FormattableMarkup($string, ['@replacement' => 'kitten']);
$text = (string) $formattable_string;
$this->assertEquals('Can I please have a kitten', $text);
$text = $formattable_string->jsonSerialize();
$this->assertEquals('Can I please have a kitten', $text);
}
/**
* @covers ::count
*/
public function testCount() {
$string = 'Can I please have a @replacement';
$formattable_string = new FormattableMarkup($string, ['@replacement' => 'kitten']);
$this->assertEquals(strlen($string), $formattable_string->count());
}
/**
* Custom error handler that saves the last error.
*
* We need this custom error handler because we cannot rely on the error to
* exception conversion as __toString is never allowed to leak any kind of
* exception.
*
* @param int $error_number
* The error number.
* @param string $error_message
* The error message.
*/
public function errorHandler($error_number, $error_message) {
$this->lastErrorNumber = $error_number;
$this->lastErrorMessage = $error_message;
}
/**
* @covers ::__toString
* @dataProvider providerTestUnexpectedPlaceholder
*/
public function testUnexpectedPlaceholder($string, $arguments, $error_number, $error_message) {
// We set a custom error handler because of https://github.com/sebastianbergmann/phpunit/issues/487
set_error_handler([$this, 'errorHandler']);
// We want this to trigger an error.
$markup = new FormattableMarkup($string, $arguments);
// Cast it to a string which will generate the errors.
$output = (string) $markup;
restore_error_handler();
// The string should not change.
$this->assertEquals($string, $output);
$this->assertEquals($error_number, $this->lastErrorNumber);
$this->assertEquals($error_message, $this->lastErrorMessage);
}
/**
* Data provider for FormattableMarkupTest::testUnexpectedPlaceholder().
*
* @return array
*/
public function providerTestUnexpectedPlaceholder() {
return [
['Non alpha starting character: ~placeholder', ['~placeholder' => 'replaced'], E_USER_ERROR, 'Invalid placeholder (~placeholder) in string: Non alpha starting character: ~placeholder'],
['Alpha starting character: placeholder', ['placeholder' => 'replaced'], E_USER_DEPRECATED, 'Invalid placeholder (placeholder) in string: Alpha starting character: placeholder'],
// Ensure that where the placeholder is located in the the string is
// irrelevant.
['placeholder', ['placeholder' => 'replaced'], E_USER_DEPRECATED, 'Invalid placeholder (placeholder) in string: placeholder'],
];
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\Tests\Component\Render;
use Drupal\Component\Render\HtmlEscapedText;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Tests\UnitTestCase;
/**
* Tests the HtmlEscapedText class.
*
* @coversDefaultClass \Drupal\Component\Render\HtmlEscapedText
* @group utility
*/
class HtmlEscapedTextTest extends UnitTestCase {
/**
* @covers ::__toString
* @covers ::jsonSerialize
*
* @dataProvider providerToString
*/
public function testToString($text, $expected, $message) {
$escapeable_string = new HtmlEscapedText($text);
$this->assertEquals($expected, (string) $escapeable_string, $message);
$this->assertEquals($expected, $escapeable_string->jsonSerialize());
}
/**
* Data provider for testToString().
*
* @see testToString()
*/
public function providerToString() {
// Checks that invalid multi-byte sequences are escaped.
$tests[] = ["Foo\xC0barbaz", 'Foobarbaz', 'Escapes invalid sequence "Foo\xC0barbaz"'];
$tests[] = ["\xc2\"", '&quot;', 'Escapes invalid sequence "\xc2\""'];
$tests[] = ["Fooÿñ", "Fooÿñ", 'Does not escape valid sequence "Fooÿñ"'];
// Checks that special characters are escaped.
$script_tag = $this->prophesize(MarkupInterface::class);
$script_tag->__toString()->willReturn('<script>');
$script_tag = $script_tag->reveal();
$tests[] = [$script_tag, '&lt;script&gt;', 'Escapes &lt;script&gt; even inside an object that implements MarkupInterface.'];
$tests[] = ["<script>", '&lt;script&gt;', 'Escapes &lt;script&gt;'];
$tests[] = ['<>&"\'', '&lt;&gt;&amp;&quot;&#039;', 'Escapes reserved HTML characters.'];
$specialchars = $this->prophesize(MarkupInterface::class);
$specialchars->__toString()->willReturn('<>&"\'');
$specialchars = $specialchars->reveal();
$tests[] = [$specialchars, '&lt;&gt;&amp;&quot;&#039;', 'Escapes reserved HTML characters even inside an object that implements MarkupInterface.'];
return $tests;
}
/**
* @covers ::count
*/
public function testCount() {
$string = 'Can I please have a <em>kitten</em>';
$escapeable_string = new HtmlEscapedText($string);
$this->assertEquals(strlen($string), $escapeable_string->count());
}
}
@@ -0,0 +1,63 @@
<?php
namespace Drupal\Tests\Component\Render;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Render\PlainTextOutput
* @group Utility
*/
class PlainTextOutputTest extends UnitTestCase {
/**
* Tests ::renderFromHtml().
*
* @param $expected
* The expected formatted value.
* @param $string
* A string to be formatted.
* @param array $args
* (optional) An associative array of replacements to make. Defaults to
* none.
*
* @covers ::renderFromHtml
* @dataProvider providerRenderFromHtml
*/
public function testRenderFromHtml($expected, $string, $args = []) {
$markup = SafeMarkup::format($string, $args);
$output = PlainTextOutput::renderFromHtml($markup);
$this->assertSame($expected, $output);
}
/**
* Data provider for ::testRenderFromHtml()
*/
public function providerRenderFromHtml() {
$data = [];
$data['simple-text'] = ['Giraffes and wombats', 'Giraffes and wombats'];
$data['simple-html'] = ['Giraffes and wombats', '<a href="/muh">Giraffes</a> and <strong>wombats</strong>'];
$data['html-with-quote'] = ['Giraffes and quote"s', '<a href="/muh">Giraffes</a> and <strong>quote"s</strong>'];
$expected = 'The <em> tag makes your text look like "this".';
$string = 'The &lt;em&gt; tag makes your text look like <em>"this"</em>.';
$data['escaped-html-with-quotes'] = [$expected, $string];
$safe_string = $this->prophesize(MarkupInterface::class);
$safe_string->__toString()->willReturn('<em>"this"</em>');
$safe_string = $safe_string->reveal();
$data['escaped-html-with-quotes-and-placeholders'] = [$expected, 'The @tag tag makes your text look like @result.', ['@tag' => '<em>', '@result' => $safe_string]];
$safe_string = $this->prophesize(MarkupInterface::class);
$safe_string->__toString()->willReturn($string);
$safe_string = $safe_string->reveal();
$data['safe-string'] = [$expected, $safe_string];
return $data;
}
}
@@ -0,0 +1,117 @@
<?php
namespace Drupal\Tests\Component\Serialization;
use Drupal\Component\Serialization\Json;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Serialization\Json
* @group Serialization
*/
class JsonTest extends UnitTestCase {
/**
* A test string with the full ASCII table.
*
* @var string
*/
protected $string;
/**
* An array of unsafe html characters which has to be encoded.
*
* @var array
*/
protected $htmlUnsafe;
/**
* An array of unsafe html characters which are already escaped.
*
* @var array
*/
protected $htmlUnsafeEscaped;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Setup a string with the full ASCII table.
// @todo: Add tests for non-ASCII characters and Unicode.
$this->string = '';
for ($i = 1; $i < 128; $i++) {
$this->string .= chr($i);
}
// Characters that must be escaped.
// We check for unescaped " separately.
$this->htmlUnsafe = ['<', '>', '\'', '&'];
// The following are the encoded forms of: < > ' & "
$this->htmlUnsafeEscaped = ['\u003C', '\u003E', '\u0027', '\u0026', '\u0022'];
}
/**
* Tests encoding for every ASCII character.
*/
public function testEncodingAscii() {
// Verify there aren't character encoding problems with the source string.
$this->assertSame(strlen($this->string), 127, 'A string with the full ASCII table has the correct length.');
foreach ($this->htmlUnsafe as $char) {
$this->assertTrue(strpos($this->string, $char) > 0, sprintf('A string with the full ASCII table includes %s.', $char));
}
}
/**
* Tests encoding length.
*/
public function testEncodingLength() {
// Verify that JSON encoding produces a string with all of the characters.
$json = Json::encode($this->string);
$this->assertTrue(strlen($json) > strlen($this->string), 'A JSON encoded string is larger than the source string.');
}
/**
* Tests end and start of the encoded string.
*/
public function testEncodingStartEnd() {
$json = Json::encode($this->string);
// The first and last characters should be ", and no others.
$this->assertTrue($json[0] == '"', 'A JSON encoded string begins with ".');
$this->assertTrue($json[strlen($json) - 1] == '"', 'A JSON encoded string ends with ".');
$this->assertTrue(substr_count($json, '"') == 2, 'A JSON encoded string contains exactly two ".');
}
/**
* Tests converting PHP variables to JSON strings and back.
*/
public function testReversibility() {
$json = Json::encode($this->string);
// Verify that encoding/decoding is reversible.
$json_decoded = Json::decode($json);
$this->assertSame($this->string, $json_decoded, 'Encoding a string to JSON and decoding back results in the original string.');
}
/**
* Test the reversibility of structured data
*/
public function testStructuredReversibility() {
// Verify reversibility for structured data. Also verify that necessary
// characters are escaped.
$source = [TRUE, FALSE, 0, 1, '0', '1', $this->string, ['key1' => $this->string, 'key2' => ['nested' => TRUE]]];
$json = Json::encode($source);
foreach ($this->htmlUnsafe as $char) {
$this->assertTrue(strpos($json, $char) === FALSE, sprintf('A JSON encoded string does not contain %s.', $char));
}
// Verify that JSON encoding escapes the HTML unsafe characters
foreach ($this->htmlUnsafeEscaped as $char) {
$this->assertTrue(strpos($json, $char) > 0, sprintf('A JSON encoded string contains %s.', $char));
}
$json_decoded = Json::decode($json);
$this->assertNotSame($source, $json, 'An array encoded in JSON is identical to the source.');
$this->assertSame($source, $json_decoded, 'Encoding structured data to JSON and decoding back not results in the original data.');
}
}
@@ -0,0 +1,84 @@
<?php
namespace Drupal\Tests\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\YamlPecl;
/**
* Tests the YamlPecl serialization implementation.
*
* @group Drupal
* @group Serialization
* @coversDefaultClass \Drupal\Component\Serialization\YamlPecl
* @requires extension yaml
*/
class YamlPeclTest extends YamlTestBase {
/**
* Tests encoding and decoding basic data structures.
*
* @covers ::encode
* @covers ::decode
* @dataProvider providerEncodeDecodeTests
*/
public function testEncodeDecode($data) {
$this->assertEquals($data, YamlPecl::decode(YamlPecl::encode($data)));
}
/**
* Tests decoding YAML node anchors.
*
* @covers ::decode
* @dataProvider providerDecodeTests
*/
public function testDecode($string, $data) {
$this->assertEquals($data, YamlPecl::decode($string));
}
/**
* Tests our encode settings.
*
* @covers ::encode
*/
public function testEncode() {
$this->assertEquals('---
foo:
bar: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sapien ex, venenatis vitae nisi eu, posuere luctus dolor. Nullam convallis
...
', YamlPecl::encode(['foo' => ['bar' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sapien ex, venenatis vitae nisi eu, posuere luctus dolor. Nullam convallis']]));
}
/**
* Tests YAML boolean callback.
*
* @param string $string
* String value for the YAML boolean.
* @param string|bool $expected
* The expected return value.
*
* @covers ::applyBooleanCallbacks
* @dataProvider providerBoolTest
*/
public function testApplyBooleanCallbacks($string, $expected) {
$this->assertEquals($expected, YamlPecl::applyBooleanCallbacks($string, 'bool', NULL));
}
/**
* @covers ::getFileExtension
*/
public function testGetFileExtension() {
$this->assertEquals('yml', YamlPecl::getFileExtension());
}
/**
* Tests that invalid YAML throws an exception.
*
* @covers ::errorHandler
*/
public function testError() {
$this->setExpectedException(InvalidDataTypeException::class);
YamlPecl::decode('foo: [ads');
}
}
@@ -0,0 +1,66 @@
<?php
namespace Drupal\Tests\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\YamlSymfony;
/**
* Tests the YamlSymfony serialization implementation.
*
* @group Drupal
* @group Serialization
* @coversDefaultClass \Drupal\Component\Serialization\YamlSymfony
*/
class YamlSymfonyTest extends YamlTestBase {
/**
* Tests encoding and decoding basic data structures.
*
* @covers ::encode
* @covers ::decode
* @dataProvider providerEncodeDecodeTests
*/
public function testEncodeDecode($data) {
$this->assertEquals($data, YamlSymfony::decode(YamlSymfony::encode($data)));
}
/**
* Tests decoding YAML node anchors.
*
* @covers ::decode
* @dataProvider providerDecodeTests
*/
public function testDecode($string, $data) {
$this->assertEquals($data, YamlSymfony::decode($string));
}
/**
* Tests our encode settings.
*
* @covers ::encode
*/
public function testEncode() {
$this->assertEquals('foo:
bar: \'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sapien ex, venenatis vitae nisi eu, posuere luctus dolor. Nullam convallis\'
', YamlSymfony::encode(['foo' => ['bar' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sapien ex, venenatis vitae nisi eu, posuere luctus dolor. Nullam convallis']]));
}
/**
* @covers ::getFileExtension
*/
public function testGetFileExtension() {
$this->assertEquals('yml', YamlSymfony::getFileExtension());
}
/**
* Tests that invalid YAML throws an exception.
*
* @covers ::decode
*/
public function testError() {
$this->setExpectedException(InvalidDataTypeException::class);
YamlSymfony::decode('foo: [ads');
}
}
@@ -0,0 +1,133 @@
<?php
namespace Drupal\Tests\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Serialization\YamlPecl;
use Drupal\Component\Serialization\YamlSymfony;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Serialization\Yaml
* @group Serialization
*/
class YamlTest extends UnitTestCase {
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $mockParser;
public function setUp() {
parent::setUp();
$this->mockParser = $this->getMockBuilder('\stdClass')
->setMethods(['encode', 'decode', 'getFileExtension'])
->getMock();
YamlParserProxy::setMock($this->mockParser);
}
public function tearDown() {
YamlParserProxy::setMock(NULL);
parent::tearDown();
}
/**
* @covers ::decode
*/
public function testDecode() {
$this->mockParser
->expects($this->once())
->method('decode');
YamlStub::decode('test');
}
/**
* @covers ::getFileExtension
*/
public function testGetFileExtension() {
$this->mockParser
->expects($this->never())
->method('getFileExtension');
$this->assertEquals('yml', YamlStub::getFileExtension());
}
/**
* Tests all YAML files are decoded in the same way with Symfony and PECL.
*
* This test is a little bit slow but it tests that we do not have any bugs in
* our YAML that might not be decoded correctly in any of our implementations.
*
* @todo This should exist as an integration test not part of our unit tests.
* https://www.drupal.org/node/2597730
*
* @requires extension yaml
* @dataProvider providerYamlFilesInCore
*/
public function testYamlFiles($file) {
$data = file_get_contents($file);
try {
$this->assertEquals(YamlSymfony::decode($data), YamlPecl::decode($data), $file);
}
catch (InvalidDataTypeException $e) {
// Provide file context to the failure so the exception message is useful.
$this->fail("Exception thrown parsing $file:\n" . $e->getMessage());
}
}
/**
* Data provider that lists all YAML files in core.
*/
public function providerYamlFilesInCore() {
$files = [];
$dirs = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/../../../../../', \RecursiveDirectoryIterator::FOLLOW_SYMLINKS));
foreach ($dirs as $dir) {
$pathname = $dir->getPathname();
// Exclude vendor.
if ($dir->getExtension() == 'yml' && strpos($pathname, '/../../../../../vendor') === FALSE) {
if (strpos($dir->getRealPath(), 'invalid_file') !== FALSE) {
// There are some intentionally invalid files provided for testing
// library API behaviours, ignore them.
continue;
}
$files[] = [$dir->getRealPath()];
}
}
return $files;
}
}
class YamlStub extends Yaml {
public static function getSerializer() {
return '\Drupal\Tests\Component\Serialization\YamlParserProxy';
}
}
class YamlParserProxy implements SerializationInterface {
/**
* @var \Drupal\Component\Serialization\SerializationInterface
*/
protected static $mock;
public static function setMock($mock) {
static::$mock = $mock;
}
public static function encode($data) {
return static::$mock->encode($data);
}
public static function decode($raw) {
return static::$mock->decode($raw);
}
public static function getFileExtension() {
return static::$mock->getFileExtension();
}
}
@@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\Component\Serialization;
/**
* Provides standard data to validate different YAML implementations.
*/
abstract class YamlTestBase extends \PHPUnit_Framework_TestCase {
/**
* Some data that should be able to be serialized.
*/
public function providerEncodeDecodeTests() {
return [
[
'foo' => 'bar',
'id' => 'schnitzel',
'ponies' => ['nope', 'thanks'],
'how' => [
'about' => 'if',
'i' => 'ask',
'nicely',
],
'the' => [
'answer' => [
'still' => 'would',
'be' => 'Y',
],
],
'how_many_times' => 123,
'should_i_ask' => FALSE,
1,
FALSE,
[1, FALSE],
[10],
[0 => '123456'],
],
[NULL]
];
}
/**
* Some data that should be able to be de-serialized.
*/
public function providerDecodeTests() {
$data = [
// NULL files.
['', NULL],
["\n", NULL],
["---\n...\n", NULL],
// Node anchors.
[
"
jquery.ui:
version: &jquery_ui 1.10.2
jquery.ui.accordion:
version: *jquery_ui
",
[
'jquery.ui' => [
'version' => '1.10.2',
],
'jquery.ui.accordion' => [
'version' => '1.10.2',
],
],
],
];
// 1.2 Bool values.
foreach ($this->providerBoolTest() as $test) {
$data[] = ['bool: ' . $test[0], ['bool' => $test[1]]];
}
$data = array_merge($data, $this->providerBoolTest());
return $data;
}
/**
* Tests different boolean serialization and de-serialization.
*/
public function providerBoolTest() {
return [
['true', TRUE],
['TRUE', TRUE],
['True', TRUE],
['y', 'y'],
['Y', 'Y'],
['false', FALSE],
['FALSE', FALSE],
['False', FALSE],
['n', 'n'],
['N', 'N'],
];
}
}
@@ -0,0 +1,188 @@
<?php
namespace Drupal\Tests\Component\Transliteration;
use Drupal\Component\Transliteration\PhpTransliteration;
use Drupal\Component\Utility\Random;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* Tests Transliteration component functionality.
*
* @group Transliteration
*
* @coversDefaultClass \Drupal\Component\Transliteration\PhpTransliteration
*/
class PhpTransliterationTest extends UnitTestCase {
/**
* Tests the PhpTransliteration::removeDiacritics() function.
*
* @param string $original
* The language code to test.
* @param string $expected
* The expected return from PhpTransliteration::removeDiacritics().
*
* @dataProvider providerTestPhpTransliterationRemoveDiacritics
*/
public function testRemoveDiacritics($original, $expected) {
$transliterator_class = new PhpTransliteration();
$result = $transliterator_class->removeDiacritics($original);
$this->assertEquals($expected, $result);
}
/**
* Provides data for self::testRemoveDiacritics().
*
* @return array
* An array of arrays, each containing the parameters for
* self::testRemoveDiacritics().
*/
public function providerTestPhpTransliterationRemoveDiacritics() {
return [
// Test all characters in the Unicode range 0x00bf to 0x017f.
['ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ', 'AAAAAAÆCEEEEIIII'],
['ÐÑÒÓÔÕÖרÙÚÛÜÝÞß', 'ÐNOOOOO×OUUUUYÞß'],
['àáâãäåæçèéêëìíîï', 'aaaaaaæceeeeiiii'],
['ðñòóôõö÷øùúûüýþÿ', 'ðnooooo÷ouuuuyþy'],
['ĀāĂ㥹ĆćĈĉĊċČčĎď', 'AaAaAaCcCcCcCcDd'],
['ĐđĒēĔĕĖėĘęĚěĜĝĞğ', 'DdEeEeEeEeEeGgGg'],
['ĠġĢģĤĥĦħĨĩĪīĬĭĮį', 'GgGgHhHhIiIiIiIi'],
['İıIJijĴĵĶķĸĹĺĻļĽľĿ', 'IiIJijJjKkĸLlLlLlL'],
['ŀŁłŃńŅņŇňʼnŊŋŌōŎŏ', 'lLlNnNnNnʼnŊŋOoOo'],
['ŐőŒœŔŕŖŗŘřŚśŜŝŞş', 'OoŒœRrRrRrSsSsSs'],
['ŠšŢţŤťŦŧŨũŪūŬŭŮů', 'SsTtTtTtUuUuUuUu'],
['ŰűŲųŴŵŶŷŸŹźŻżŽž', 'UuUuWwYyYZzZzZz'],
// Test all characters in the Unicode range 0x01CD to 0x024F.
['ǍǎǏ', 'AaI'],
['ǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟ', 'iOoUuUuUuUuUuǝAa'],
['ǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯ', 'AaǢǣGgGgKkOoOoǮǯ'],
['ǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿ', 'jDZDzdzGgǶǷNnAaǼǽOo'],
['ȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏ', 'AaAaEeEeIiIiOoOo'],
['ȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟ', 'RrRrUuUuSsTtȜȝHh'],
['ȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯ', 'ȠȡȢȣZzAaEeOoOoOo'],
['ȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿ', 'OoYylntjȸȹACcLTs'],
['ɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏ', 'zɁɂBUɅEeJjQqRrYy'],
];
}
/**
* Tests the PhpTransliteration class.
*
* @param string $langcode
* The language code to test.
* @param string $original
* The original string.
* @param string $expected
* The expected return from PhpTransliteration::transliterate().
* @param string $unknown_character
* (optional) The character to substitute for characters in $string without
* transliterated equivalents. Defaults to '?'.
* @param int $max_length
* (optional) If provided, return at most this many characters, ensuring
* that the transliteration does not split in the middle of an input
* character's transliteration.
*
* @dataProvider providerTestPhpTransliteration
*/
public function testPhpTransliteration($langcode, $original, $expected, $unknown_character = '?', $max_length = NULL) {
$transliterator_class = new PhpTransliteration();
$actual = $transliterator_class->transliterate($original, $langcode, $unknown_character, $max_length);
$this->assertSame($expected, $actual);
}
/**
* Provides data for self::testPhpTransliteration().
*
* @return array
* An array of arrays, each containing the parameters for
* self::testPhpTransliteration().
*/
public function providerTestPhpTransliteration() {
$random_generator = new Random();
$random = $random_generator->string(10);
// Make some strings with two, three, and four-byte characters for testing.
// Note that the 3-byte character is overridden by the 'kg' language.
$two_byte = 'Ä Ö Ü Å Ø äöüåøhello';
// This is a Cyrrillic character that looks something like a u. See
// http://www.unicode.org/charts/PDF/U0400.pdf
$three_byte = html_entity_decode('&#x446;', ENT_NOQUOTES, 'UTF-8');
// This is a Canadian Aboriginal character like a triangle. See
// http://www.unicode.org/charts/PDF/U1400.pdf
$four_byte = html_entity_decode('&#x1411;', ENT_NOQUOTES, 'UTF-8');
// These are two Gothic alphabet letters. See
// http://wikipedia.org/wiki/Gothic_alphabet
// They are not in our tables, but should at least give us '?' (unknown).
$five_byte = html_entity_decode('&#x10330;&#x10338;', ENT_NOQUOTES, 'UTF-8');
return [
// Each test case is (language code, input, output).
// Test ASCII in English.
['en', $random, $random],
// Test ASCII in some other language with no overrides.
['fr', $random, $random],
// Test 3 and 4-byte characters in a language without overrides.
// Note: if the data tables change, these will need to change too! They
// are set up to test that data table loading works, so values come
// directly from the data files.
['fr', $three_byte, 'c'],
['fr', $four_byte, 'wii'],
// Test 5-byte characters.
['en', $five_byte, '??'],
// Test a language with no overrides.
['en', $two_byte, 'A O U A O aouaohello'],
// Test language overrides provided by core.
['de', $two_byte, 'Ae Oe Ue A O aeoeueaohello'],
['de', $random, $random],
['dk', $two_byte, 'A O U Aa Oe aouaaoehello'],
['dk', $random, $random],
['kg', $three_byte, 'ts'],
// Test strings in some other languages.
// Turkish, provided by drupal.org user Kartagis.
['tr', 'Abayı serdiler bize. Söyleyeceğim yüzlerine. Sanırım hepimiz aynı şeyi düşünüyoruz.', 'Abayi serdiler bize. Soyleyecegim yuzlerine. Sanirim hepimiz ayni seyi dusunuyoruz.'],
// Illegal/unknown unicode.
['en', chr(0xF8) . chr(0x80) . chr(0x80) . chr(0x80) . chr(0x80), '?'],
// Max length.
['de', $two_byte, 'Ae Oe', '?', 5],
];
}
/**
* Tests the transliteration with max length.
*/
public function testTransliterationWithMaxLength() {
$transliteration = new PhpTransliteration();
// Test with max length, using German. It should never split up the
// transliteration of a single character.
$input = 'Ä Ö Ü Å Ø äöüåøhello';
$trunc_output = 'Ae Oe Ue A O aeoe';
$this->assertSame($trunc_output, $transliteration->transliterate($input, 'de', '?', 17), 'Truncating to 17 characters works');
$this->assertSame($trunc_output, $transliteration->transliterate($input, 'de', '?', 18), 'Truncating to 18 characters works');
}
/**
* Tests inclusion is safe.
*
* @covers ::readLanguageOverrides
*/
public function testSafeInclude() {
// The overrides in the transliteration data directory transliterates 0x82
// into "safe" but the overrides one directory higher transliterates the
// same character into "security hole". So by using "../index" as the
// language code we can test the ../ is stripped from the langcode.
vfsStream::setup('transliteration', NULL, [
'index.php' => '<?php $overrides = ["../index" => [0x82 => "security hole"]];',
'dir' => [
'index.php' => '<?php $overrides = ["../index" => [0x82 => "safe"]];',
],
]);
$transliteration = new PhpTransliteration(vfsStream::url('transliteration/dir'));
$transliterated = $transliteration->transliterate(chr(0xC2) . chr(0x82), '../index');
$this->assertSame($transliterated, 'safe');
}
}
@@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Utility\ArgumentsResolverTest.
*/
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\ArgumentsResolver;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Utility\ArgumentsResolver
* @group Access
*/
class ArgumentsResolverTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
}
/**
* Tests the getArgument() method.
*
* @dataProvider providerTestGetArgument
*/
public function testGetArgument($callable, $scalars, $objects, $wildcards, $expected) {
$arguments = (new ArgumentsResolver($scalars, $objects, $wildcards))->getArguments($callable);
$this->assertSame($expected, $arguments);
}
/**
* Provides test data to testGetArgument().
*/
public function providerTestGetArgument() {
$data = [];
// Test an optional parameter with no provided value.
$data[] = [
function($foo = 'foo') {}, [], [], [] , ['foo'],
];
// Test an optional parameter with a provided value.
$data[] = [
function($foo = 'foo') {}, ['foo' => 'bar'], [], [], ['bar'],
];
// Test with a provided value.
$data[] = [
function($foo) {}, ['foo' => 'bar'], [], [], ['bar'],
];
// Test with an explicitly NULL value.
$data[] = [
function($foo) {}, [], ['foo' => NULL], [], [NULL],
];
// Test with a raw value that overrides the provided upcast value, since
// it is not typehinted.
$scalars = ['foo' => 'baz'];
$objects = ['foo' => new \stdClass()];
$data[] = [
function($foo) {}, $scalars, $objects, [], ['baz'],
];
return $data;
}
/**
* Tests getArgument() with an object.
*/
public function testGetArgumentObject() {
$callable = function(\stdClass $object) {};
$object = new \stdClass();
$arguments = (new ArgumentsResolver([], ['object' => $object], []))->getArguments($callable);
$this->assertSame([$object], $arguments);
}
/**
* Tests getArgument() with a wildcard object for a parameter with a custom name.
*/
public function testGetWildcardArgument() {
$callable = function(\stdClass $custom_name) {};
$object = new \stdClass();
$arguments = (new ArgumentsResolver([], [], [$object]))->getArguments($callable);
$this->assertSame([$object], $arguments);
}
/**
* Tests getArgument() with a Route, Request, and Account object.
*/
public function testGetArgumentOrder() {
$a1 = $this->getMock('\Drupal\Tests\Component\Utility\TestInterface1');
$a2 = $this->getMock('\Drupal\Tests\Component\Utility\TestClass');
$a3 = $this->getMock('\Drupal\Tests\Component\Utility\TestInterface2');
$objects = [
't1' => $a1,
'tc' => $a2,
];
$wildcards = [$a3];
$resolver = new ArgumentsResolver([], $objects, $wildcards);
$callable = function(TestInterface1 $t1, TestClass $tc, TestInterface2 $t2) {};
$arguments = $resolver->getArguments($callable);
$this->assertSame([$a1, $a2, $a3], $arguments);
// Test again, but with the arguments in a different order.
$callable = function(TestInterface2 $t2, TestClass $tc, TestInterface1 $t1) {};
$arguments = $resolver->getArguments($callable);
$this->assertSame([$a3, $a2, $a1], $arguments);
}
/**
* Tests getArgument() with a wildcard parameter with no typehint.
*
* Without the typehint, the wildcard object will not be passed to the callable.
*/
public function testGetWildcardArgumentNoTypehint() {
$a = $this->getMock('\Drupal\Tests\Component\Utility\TestInterface1');
$wildcards = [$a];
$resolver = new ArgumentsResolver([], [], $wildcards);
$callable = function($route) {};
$this->setExpectedException(\RuntimeException::class, 'requires a value for the "$route" argument.');
$resolver->getArguments($callable);
}
/**
* Tests getArgument() with a named parameter with no typehint and a value.
*
* Without the typehint, passing a value to a named parameter will still
* receive the provided value.
*/
public function testGetArgumentRouteNoTypehintAndValue() {
$scalars = ['route' => 'foo'];
$resolver = new ArgumentsResolver($scalars, [], []);
$callable = function($route) {};
$arguments = $resolver->getArguments($callable);
$this->assertSame(['foo'], $arguments);
}
/**
* Tests handleUnresolvedArgument() for a scalar argument.
*/
public function testHandleNotUpcastedArgument() {
$objects = ['foo' => 'bar'];
$scalars = ['foo' => 'baz'];
$resolver = new ArgumentsResolver($scalars, $objects, []);
$callable = function(\stdClass $foo) {};
$this->setExpectedException(\RuntimeException::class, 'requires a value for the "$foo" argument.');
$resolver->getArguments($callable);
}
/**
* Tests handleUnresolvedArgument() for missing arguments.
*
* @dataProvider providerTestHandleUnresolvedArgument
*/
public function testHandleUnresolvedArgument($callable) {
$resolver = new ArgumentsResolver([], [], []);
$this->setExpectedException(\RuntimeException::class, 'requires a value for the "$foo" argument.');
$resolver->getArguments($callable);
}
/**
* Provides test data to testHandleUnresolvedArgument().
*/
public function providerTestHandleUnresolvedArgument() {
$data = [];
$data[] = [function($foo) {}];
$data[] = [[new TestClass(), 'access']];
$data[] = ['Drupal\Tests\Component\Utility\test_access_arguments_resolver_access'];
return $data;
}
}
/**
* Provides a test class.
*/
class TestClass {
public function access($foo) {
}
}
/**
* Provides a test interface.
*/
interface TestInterface1 {
}
/**
* Provides a different test interface.
*/
interface TestInterface2 {
}
function test_access_arguments_resolver_access($foo) {
}
@@ -0,0 +1,60 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Bytes;
use Drupal\Tests\UnitTestCase;
/**
* Tests bytes size parsing helper methods.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Bytes
*/
class BytesTest extends UnitTestCase {
/**
* Tests \Drupal\Component\Utility\Bytes::toInt().
*
* @param int $size
* The value for the size argument for
* \Drupal\Component\Utility\Bytes::toInt().
* @param int $expected_int
* The expected return value from
* \Drupal\Component\Utility\Bytes::toInt().
*
* @dataProvider providerTestToInt
* @covers ::toInt
*/
public function testToInt($size, $expected_int) {
$this->assertEquals($expected_int, Bytes::toInt($size));
}
/**
* Provides data for testToInt.
*
* @return array
* An array of arrays, each containing the argument for
* \Drupal\Component\Utility\Bytes::toInt(): size, and the expected return
* value.
*/
public function providerTestToInt() {
return [
['1', 1],
['1 byte', 1],
['1 KB' , Bytes::KILOBYTE],
['1 MB' , pow(Bytes::KILOBYTE, 2)],
['1 GB' , pow(Bytes::KILOBYTE, 3)],
['1 TB' , pow(Bytes::KILOBYTE, 4)],
['1 PB' , pow(Bytes::KILOBYTE, 5)],
['1 EB' , pow(Bytes::KILOBYTE, 6)],
['1 ZB' , pow(Bytes::KILOBYTE, 7)],
['1 YB' , pow(Bytes::KILOBYTE, 8)],
['23476892 bytes', 23476892],
['76MRandomStringThatShouldBeIgnoredByParseSize.', 79691776], // 76 MB
['76.24 Giggabyte', 81862076662], // 76.24 GB (with typo)
];
}
}
@@ -0,0 +1,121 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Color;
use Drupal\Tests\UnitTestCase;
/**
* Tests Color utility class conversions.
*
* @group Utility
*/
class ColorTest extends UnitTestCase {
/**
* Tests Color::hexToRgb().
*
* @param string $value
* The hex color value.
* @param string $expected
* The expected rgb color value.
* @param bool $invalid
* Whether this value is invalid and exception should be expected.
*
* @dataProvider providerTestHexToRgb
*/
public function testHexToRgb($value, $expected, $invalid = FALSE) {
if ($invalid) {
$this->setExpectedException('InvalidArgumentException');
}
$this->assertSame($expected, Color::hexToRgb($value));
}
/**
* Data provider for testHexToRgb().
*
* @see testHexToRgb()
*
* @return array
* An array of arrays containing:
* - The hex color value.
* - The rgb color array value.
* - (optional) Boolean indicating invalid status. Defaults to FALSE.
*/
public function providerTestHexToRgb() {
$invalid = [];
// Any invalid arguments should throw an exception.
foreach (['', '-1', '1', '12', '12345', '1234567', '123456789', '123456789a', 'foo'] as $value) {
$invalid[] = [$value, '', TRUE];
}
// Duplicate all invalid value tests with additional '#' prefix.
// The '#' prefix inherently turns the data type into a string.
foreach ($invalid as $value) {
$invalid[] = ['#' . $value[0], '', TRUE];
}
// Add invalid data types (hex value must be a string).
foreach ([
1, 12, 1234, 12345, 123456, 1234567, 12345678, 123456789, 123456789,
-1, PHP_INT_MAX, PHP_INT_MAX + 1, -PHP_INT_MAX, 0x0, 0x010
] as $value) {
$invalid[] = [$value, '', TRUE];
}
// And some valid values.
$valid = [
// Shorthands without alpha.
['hex' => '#000', 'rgb' => ['red' => 0, 'green' => 0, 'blue' => 0]],
['hex' => '#fff', 'rgb' => ['red' => 255, 'green' => 255, 'blue' => 255]],
['hex' => '#abc', 'rgb' => ['red' => 170, 'green' => 187, 'blue' => 204]],
['hex' => 'cba', 'rgb' => ['red' => 204, 'green' => 187, 'blue' => 170]],
// Full without alpha.
['hex' => '#000000', 'rgb' => ['red' => 0, 'green' => 0, 'blue' => 0]],
['hex' => '#ffffff', 'rgb' => ['red' => 255, 'green' => 255, 'blue' => 255]],
['hex' => '#010203', 'rgb' => ['red' => 1, 'green' => 2, 'blue' => 3]],
];
return array_merge($invalid, $valid);
}
/**
* Tests Color::rgbToHex().
*
* @param string $value
* The rgb color value.
* @param string $expected
* The expected hex color value.
*
* @dataProvider providerTestRbgToHex
*/
public function testRgbToHex($value, $expected) {
$this->assertSame($expected, Color::rgbToHex($value));
}
/**
* Data provider for testRgbToHex().
*
* @see testRgbToHex()
*
* @return array
* An array of arrays containing:
* - The rgb color array value.
* - The hex color value.
*/
public function providerTestRbgToHex() {
// Input using named RGB array (e.g., as returned by Color::hexToRgb()).
$tests = [
[['red' => 0, 'green' => 0, 'blue' => 0], '#000000'],
[['red' => 255, 'green' => 255, 'blue' => 255], '#ffffff'],
[['red' => 119, 'green' => 119, 'blue' => 119], '#777777'],
[['red' => 1, 'green' => 2, 'blue' => 3], '#010203'],
];
// Input using indexed RGB array (e.g.: array(10, 10, 10)).
foreach ($tests as $test) {
$tests[] = [array_values($test[0]), $test[1]];
}
// Input using CSS RGB string notation (e.g.: 10, 10, 10).
foreach ($tests as $test) {
$tests[] = [implode(', ', $test[0]), $test[1]];
}
return $tests;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\Crypt;
/**
* Tests random byte generation fallback exception situations.
*
* @group Utility
*
* @runTestsInSeparateProcesses
*
* @coversDefaultClass \Drupal\Component\Utility\Crypt
*/
class CryptRandomFallbackTest extends UnitTestCase {
static protected $functionCalled = 0;
/**
* Allows the test to confirm that the namespaced random_bytes() was called.
*/
public static function functionCalled() {
static::$functionCalled++;
}
/**
* Tests random byte generation using the fallback generator.
*
* If the call to random_bytes() throws an exception, Crypt::random_bytes()
* should still return a useful string of random bytes.
*
* @covers ::randomBytes
*
* @see \Drupal\Tests\Component\Utility\CryptTest::testRandomBytes()
*/
public function testRandomBytesFallback() {
// This loop is a copy of
// \Drupal\Tests\Component\Utility\CryptTest::testRandomBytes().
for ($i = 0; $i < 10; $i++) {
$count = rand(10, 10000);
// Check that different values are being generated.
$this->assertNotEquals(Crypt::randomBytes($count), Crypt::randomBytes($count));
// Check the length.
$this->assertEquals($count, strlen(Crypt::randomBytes($count)));
}
$this->assertEquals(30, static::$functionCalled, 'The namespaced function was called the expected number of times.');
}
}
namespace Drupal\Component\Utility;
use \Drupal\Tests\Component\Utility\CryptRandomFallbackTest;
/**
* Defines a function in same namespace as Drupal\Component\Utility\Crypt.
*
* Forces throwing an exception in this test environment because the function
* in the namespace is used in preference to the global function.
*
* @param int $count
* Matches the global function definition.
*
* @throws \Exception
*/
function random_bytes($count) {
CryptRandomFallbackTest::functionCalled();
throw new \Exception($count);
}
@@ -0,0 +1,152 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\Crypt;
/**
* Tests random byte generation.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Crypt
*/
class CryptTest extends UnitTestCase {
/**
* Tests random byte generation.
*
* @covers ::randomBytes
*
* @see \Drupal\Tests\Component\Utility\CryptRandomFallbackTest::testRandomBytesFallback
*/
public function testRandomBytes() {
for ($i = 1; $i < 10; $i++) {
$count = rand(10, 10000);
// Check that different values are being generated.
$this->assertNotEquals(Crypt::randomBytes($count), Crypt::randomBytes($count));
// Check the length.
$this->assertEquals(strlen(Crypt::randomBytes($count)), $count);
}
}
/**
* Tests hash generation.
*
* @dataProvider providerTestHashBase64
* @covers ::hashBase64
*
* @param string $data
* Data to hash.
* @param string $expected_hash
* Expected result from hashing $data.
*/
public function testHashBase64($data, $expected_hash) {
$hash = Crypt::hashBase64($data);
$this->assertEquals($expected_hash, $hash, 'The correct hash was not calculated.');
}
/**
* Tests HMAC generation.
*
* @dataProvider providerTestHmacBase64
* @covers ::hmacBase64
*
* @param string $data
* Data to hash.
* @param string $key
* Key to use in hashing process.
* @param string $expected_hmac
* Expected result from hashing $data using $key.
*/
public function testHmacBase64($data, $key, $expected_hmac) {
$hmac = Crypt::hmacBase64($data, $key);
$this->assertEquals($expected_hmac, $hmac, 'The correct hmac was not calculated.');
}
/**
* Tests the hmacBase64 method with invalid parameters.
*
* @dataProvider providerTestHmacBase64Invalid
* @covers ::hmacBase64
*
* @param string $data
* Data to hash.
* @param string $key
* Key to use in hashing process.
*/
public function testHmacBase64Invalid($data, $key) {
$this->setExpectedException(\InvalidArgumentException::class);
Crypt::hmacBase64($data, $key);
}
/**
* Provides data for self::testHashBase64().
*
* @return array Test data.
*/
public function providerTestHashBase64() {
return [
[
'data' => 'The SHA (Secure Hash Algorithm) is one of a number of cryptographic hash functions. A cryptographic hash is like a signature for a text or a data file. SHA-256 algorithm generates an almost-unique, fixed size 256-bit (32-byte) hash. Hash is a one way function it cannot be decrypted back. This makes it suitable for password validation, challenge hash authentication, anti-tamper, digital signatures.',
'expectedHash' => '034rT6smZAVRxpq8O98cFFNLIVx_Ph1EwLZQKcmRR_s',
],
[
'data' => 'SHA-256 is one of the successor hash functions to SHA-1, and is one of the strongest hash functions available.',
'expected_hash' => 'yuqkDDYqprL71k4xIb6K6D7n76xldO4jseRhEkEE6SI',
],
];
}
/**
* Provides data for self::testHmacBase64().
*
* @return array Test data.
*/
public function providerTestHmacBase64() {
return [
[
'data' => 'Calculates a base-64 encoded, URL-safe sha-256 hmac.',
'key' => 'secret-key',
'expected_hmac' => '2AaH63zwjhekWZlEpAiufyfhAHIzbQhl9Hd9oCi3_c8',
],
];
}
/**
* Provides data for self::testHmacBase64().
*
* @return array Test data.
*/
public function providerTestHmacBase64Invalid() {
return [
[new \stdClass(), new \stdClass()],
[new \stdClass(), 'string'],
[new \stdClass(), 1],
[new \stdClass(), 0],
[NULL, new \stdClass()],
['string', new \stdClass()],
[1, new \stdClass()],
[0, new \stdClass()],
[[], []],
[[], NULL],
[[], 'string'],
[[], 1],
[[], 0],
[NULL, []],
[1, []],
[0, []],
['string', []],
[[], NULL],
[NULL, NULL],
[NULL, 'string'],
[NULL, 1],
[NULL, 0],
[1, NULL],
[0, NULL],
['string', NULL],
];
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Environment;
use Drupal\Tests\UnitTestCase;
/**
* Test PHP Environment helper methods.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Environment
*/
class EnvironmentTest extends UnitTestCase {
/**
* Tests \Drupal\Component\Utility\Environment::checkMemoryLimit().
*
* @dataProvider providerTestCheckMemoryLimit
* @covers ::checkMemoryLimit
*
* @param string $required
* The required memory argument for
* \Drupal\Component\Utility\Environment::checkMemoryLimit().
* @param string $custom_memory_limit
* The custom memory limit argument for
* \Drupal\Component\Utility\Environment::checkMemoryLimit().
* @param bool $expected
* The expected return value from
* \Drupal\Component\Utility\Environment::checkMemoryLimit().
*/
public function testCheckMemoryLimit($required, $custom_memory_limit, $expected) {
$actual = Environment::checkMemoryLimit($required, $custom_memory_limit);
$this->assertEquals($expected, $actual);
}
/**
* Provides data for testCheckMemoryLimit().
*
* @return array
* An array of arrays, each containing the arguments for
* \Drupal\Component\Utility\Environment::checkMemoryLimit():
* required and memory_limit, and the expected return value.
*/
public function providerTestCheckMemoryLimit() {
$memory_limit = ini_get('memory_limit');
$twice_avail_memory = ($memory_limit * 2) . 'MB';
return [
// Minimal amount of memory should be available.
['30MB', NULL, TRUE],
// Exceed a custom (unlimited) memory limit.
[$twice_avail_memory, -1, TRUE],
// Exceed a custom memory limit.
['30MB', '16MB', FALSE],
// Available = required.
['30MB', '30MB', TRUE],
];
}
}
@@ -0,0 +1,392 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Html;
use Drupal\Tests\UnitTestCase;
/**
* Tests \Drupal\Component\Utility\Html.
*
* @group Common
*
* @coversDefaultClass \Drupal\Component\Utility\Html
*/
class HtmlTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$property = new \ReflectionProperty('Drupal\Component\Utility\Html', 'seenIdsInit');
$property->setAccessible(TRUE);
$property->setValue(NULL);
}
/**
* Tests the Html::cleanCssIdentifier() method.
*
* @param string $expected
* The expected result.
* @param string $source
* The string being transformed to an ID.
* @param array|null $filter
* (optional) An array of string replacements to use on the identifier. If
* NULL, no filter will be passed and a default will be used.
*
* @dataProvider providerTestCleanCssIdentifier
*
* @covers ::cleanCssIdentifier
*/
public function testCleanCssIdentifier($expected, $source, $filter = NULL) {
if ($filter !== NULL) {
$this->assertSame($expected, Html::cleanCssIdentifier($source, $filter));
}
else {
$this->assertSame($expected, Html::cleanCssIdentifier($source));
}
}
/**
* Provides test data for testCleanCssIdentifier().
*
* @return array
* Test data.
*/
public function providerTestCleanCssIdentifier() {
$id1 = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789';
$id2 = '¡¢£¤¥';
$id3 = 'css__identifier__with__double__underscores';
return [
// Verify that no valid ASCII characters are stripped from the identifier.
[$id1, $id1, []],
// Verify that valid UTF-8 characters are not stripped from the identifier.
[$id2, $id2, []],
// Verify that invalid characters (including non-breaking space) are stripped from the identifier.
[$id3, $id3],
// Verify that double underscores are not stripped from the identifier.
['invalididentifier', 'invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', []],
// Verify that an identifier starting with a digit is replaced.
['_cssidentifier', '1cssidentifier', []],
// Verify that an identifier starting with a hyphen followed by a digit is
// replaced.
['__cssidentifier', '-1cssidentifier', []],
// Verify that an identifier starting with two hyphens is replaced.
['__cssidentifier', '--cssidentifier', []],
// Verify that passing double underscores as a filter is processed.
['_cssidentifier', '__cssidentifier', ['__' => '_']],
];
}
/**
* Tests that Html::getClass() cleans the class name properly.
*
* @coversDefaultClass ::getClass
*/
public function testHtmlClass() {
// Verify Drupal coding standards are enforced.
$this->assertSame(Html::getClass('CLASS NAME_[Ü]'), 'class-name--ü', 'Enforce Drupal coding standards.');
}
/**
* Tests the Html::getUniqueId() method.
*
* @param string $expected
* The expected result.
* @param string $source
* The string being transformed to an ID.
* @param bool $reset
* (optional) If TRUE, reset the list of seen IDs. Defaults to FALSE.
*
* @dataProvider providerTestHtmlGetUniqueId
*
* @covers ::getUniqueId
*/
public function testHtmlGetUniqueId($expected, $source, $reset = FALSE) {
if ($reset) {
Html::resetSeenIds();
}
$this->assertSame($expected, Html::getUniqueId($source));
}
/**
* Provides test data for testHtmlGetId().
*
* @return array
* Test data.
*/
public function providerTestHtmlGetUniqueId() {
$id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
return [
// Verify that letters, digits, and hyphens are not stripped from the ID.
[$id, $id],
// Verify that invalid characters are stripped from the ID.
['invalididentifier', 'invalid,./:@\\^`{Üidentifier'],
// Verify Drupal coding standards are enforced.
['id-name-1', 'ID NAME_[1]'],
// Verify that a repeated ID is made unique.
['test-unique-id', 'test-unique-id', TRUE],
['test-unique-id--2', 'test-unique-id'],
['test-unique-id--3', 'test-unique-id'],
];
}
/**
* Tests the Html::getUniqueId() method.
*
* @param string $expected
* The expected result.
* @param string $source
* The string being transformed to an ID.
*
* @dataProvider providerTestHtmlGetUniqueIdWithAjaxIds
*
* @covers ::getUniqueId
*/
public function testHtmlGetUniqueIdWithAjaxIds($expected, $source) {
Html::setIsAjax(TRUE);
$id = Html::getUniqueId($source);
// Note, we truncate two hyphens at the end.
// @see \Drupal\Component\Utility\Html::getId()
if (strpos($source, '--') !== FALSE) {
$random_suffix = substr($id, strlen($source) + 1);
}
else {
$random_suffix = substr($id, strlen($source) + 2);
}
$expected = $expected . $random_suffix;
$this->assertSame($expected, $id);
}
/**
* Provides test data for testHtmlGetId().
*
* @return array
* Test data.
*/
public function providerTestHtmlGetUniqueIdWithAjaxIds() {
return [
['test-unique-id1--', 'test-unique-id1'],
// Note, we truncate two hyphens at the end.
// @see \Drupal\Component\Utility\Html::getId()
['test-unique-id1---', 'test-unique-id1--'],
['test-unique-id2--', 'test-unique-id2'],
];
}
/**
* Tests the Html::getUniqueId() method.
*
* @param string $expected
* The expected result.
* @param string $source
* The string being transformed to an ID.
*
* @dataProvider providerTestHtmlGetId
*
* @covers ::getId
*/
public function testHtmlGetId($expected, $source) {
Html::setIsAjax(FALSE);
$this->assertSame($expected, Html::getId($source));
}
/**
* Provides test data for testHtmlGetId().
*
* @return array
* Test data.
*/
public function providerTestHtmlGetId() {
$id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
return [
// Verify that letters, digits, and hyphens are not stripped from the ID.
[$id, $id],
// Verify that invalid characters are stripped from the ID.
['invalididentifier', 'invalid,./:@\\^`{Üidentifier'],
// Verify Drupal coding standards are enforced.
['id-name-1', 'ID NAME_[1]'],
// Verify that a repeated ID is made unique.
['test-unique-id', 'test-unique-id'],
['test-unique-id', 'test-unique-id'],
];
}
/**
* Tests Html::decodeEntities().
*
* @dataProvider providerDecodeEntities
* @covers ::decodeEntities
*/
public function testDecodeEntities($text, $expected) {
$this->assertEquals($expected, Html::decodeEntities($text));
}
/**
* Data provider for testDecodeEntities().
*
* @see testDecodeEntities()
*/
public function providerDecodeEntities() {
return [
['Drupal', 'Drupal'],
['<script>', '<script>'],
['&lt;script&gt;', '<script>'],
['&#60;script&#62;', '<script>'],
['&amp;lt;script&amp;gt;', '&lt;script&gt;'],
['"', '"'],
['&#34;', '"'],
['&amp;#34;', '&#34;'],
['&quot;', '"'],
['&amp;quot;', '&quot;'],
["'", "'"],
['&#39;', "'"],
['&amp;#39;', '&#39;'],
['©', '©'],
['&copy;', '©'],
['&#169;', '©'],
['→', '→'],
['&#8594;', '→'],
['➼', '➼'],
['&#10172;', '➼'],
['&euro;', '€'],
];
}
/**
* Tests Html::escape().
*
* @dataProvider providerEscape
* @covers ::escape
*/
public function testEscape($expected, $text) {
$this->assertEquals($expected, Html::escape($text));
}
/**
* Data provider for testEscape().
*
* @see testEscape()
*/
public function providerEscape() {
return [
['Drupal', 'Drupal'],
['&lt;script&gt;', '<script>'],
['&amp;lt;script&amp;gt;', '&lt;script&gt;'],
['&amp;#34;', '&#34;'],
['&quot;', '"'],
['&amp;quot;', '&quot;'],
['&#039;', "'"],
['&amp;#039;', '&#039;'],
['©', '©'],
['→', '→'],
['➼', '➼'],
['€', '€'],
['Drupal', "Drup\x80al"],
];
}
/**
* Tests relationship between escaping and decoding HTML entities.
*
* @covers ::decodeEntities
* @covers ::escape
*/
public function testDecodeEntitiesAndEscape() {
$string = "<em>répét&eacute;</em>";
$escaped = Html::escape($string);
$this->assertSame('&lt;em&gt;répét&amp;eacute;&lt;/em&gt;', $escaped);
$decoded = Html::decodeEntities($escaped);
$this->assertSame('<em>répét&eacute;</em>', $decoded);
$decoded = Html::decodeEntities($decoded);
$this->assertSame('<em>répété</em>', $decoded);
$escaped = Html::escape($decoded);
$this->assertSame('&lt;em&gt;répété&lt;/em&gt;', $escaped);
}
/**
* Tests Html::serialize().
*
* Resolves an issue by where an empty DOMDocument object sent to serialization would
* cause errors in getElementsByTagName() in the serialization function.
*
* @covers ::serialize
*/
public function testSerialize() {
$document = new \DOMDocument();
$result = Html::serialize($document);
$this->assertSame('', $result);
}
/**
* @covers ::transformRootRelativeUrlsToAbsolute
* @dataProvider providerTestTransformRootRelativeUrlsToAbsolute
*/
public function testTransformRootRelativeUrlsToAbsolute($html, $scheme_and_host, $expected_html) {
$this->assertSame($expected_html ?: $html, Html::transformRootRelativeUrlsToAbsolute($html, $scheme_and_host));
}
/**
* @covers ::transformRootRelativeUrlsToAbsolute
* @dataProvider providerTestTransformRootRelativeUrlsToAbsoluteAssertion
*/
public function testTransformRootRelativeUrlsToAbsoluteAssertion($scheme_and_host) {
$this->setExpectedException(\AssertionError::class);
Html::transformRootRelativeUrlsToAbsolute('', $scheme_and_host);
}
/**
* Provides test data for testTransformRootRelativeUrlsToAbsolute().
*
* @return array
* Test data.
*/
public function providerTestTransformRootRelativeUrlsToAbsolute() {
$data = [];
// One random tag name.
$tag_name = strtolower($this->randomMachineName());
// A site installed either in the root of a domain or a subdirectory.
$base_paths = ['/', '/subdir/' . $this->randomMachineName() . '/'];
foreach ($base_paths as $base_path) {
// The only attribute that has more than just a URL as its value, is
// 'srcset', so special-case it.
$data += [
"$tag_name, srcset, $base_path: root-relative" => ["<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, {$base_path}root-relative 300w\">root-relative test</$tag_name>", 'http://example.com', "<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, http://example.com{$base_path}root-relative 300w\">root-relative test</$tag_name>"],
"$tag_name, srcset, $base_path: protocol-relative" => ["<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, //example.com{$base_path}protocol-relative 300w\">protocol-relative test</$tag_name>", 'http://example.com', FALSE],
"$tag_name, srcset, $base_path: absolute" => ["<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, http://example.com{$base_path}absolute 300w\">absolute test</$tag_name>", 'http://example.com', FALSE],
];
foreach (['href', 'poster', 'src', 'cite', 'data', 'action', 'formaction', 'about'] as $attribute) {
$data += [
"$tag_name, $attribute, $base_path: root-relative" => ["<$tag_name $attribute=\"{$base_path}root-relative\">root-relative test</$tag_name>", 'http://example.com', "<$tag_name $attribute=\"http://example.com{$base_path}root-relative\">root-relative test</$tag_name>"],
"$tag_name, $attribute, $base_path: protocol-relative" => ["<$tag_name $attribute=\"//example.com{$base_path}protocol-relative\">protocol-relative test</$tag_name>", 'http://example.com', FALSE],
"$tag_name, $attribute, $base_path: absolute" => ["<$tag_name $attribute=\"http://example.com{$base_path}absolute\">absolute test</$tag_name>", 'http://example.com', FALSE],
];
}
}
return $data;
}
/**
* Provides test data for testTransformRootRelativeUrlsToAbsoluteAssertion().
*
* @return array
* Test data.
*/
public function providerTestTransformRootRelativeUrlsToAbsoluteAssertion() {
return [
'only relative path' => ['llama'],
'only root-relative path' => ['/llama'],
'host and path' => ['example.com/llama'],
'scheme, host and path' => ['http://example.com/llama'],
];
}
}
@@ -0,0 +1,158 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Image;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Utility\Image
* @group Image
*/
class ImageTest extends UnitTestCase {
/**
* Tests all control flow branches in image_dimensions_scale().
*
* @dataProvider providerTestScaleDimensions
*/
public function testScaleDimensions($input, $output) {
// Process the test dataset.
$return_value = Image::scaleDimensions($input['dimensions'], $input['width'], $input['height'], $input['upscale']);
// Check the width.
$this->assertEquals($output['dimensions']['width'], $input['dimensions']['width'], sprintf('Computed width (%s) does not equal expected width (%s)', $output['dimensions']['width'], $input['dimensions']['width']));
// Check the height.
$this->assertEquals($output['dimensions']['height'], $input['dimensions']['height'], sprintf('Computed height (%s) does not equal expected height (%s)', $output['dimensions']['height'], $input['dimensions']['height']));
// Check the return value.
$this->assertEquals($output['return_value'], $return_value, 'Incorrect return value.');
}
/**
* Provides data for image dimension scale tests.
*
* @return array
* Keyed array containing:
* - 'input' - Array which contains input for Image::scaleDimensions().
* - 'output' - Array which contains expected output after passing
* through Image::scaleDimensions. Also contains a boolean
* 'return_value' which should match the expected return value.
*
* @see testScaleDimensions()
*/
public function providerTestScaleDimensions() {
// Define input / output datasets to test different branch conditions.
$tests = [];
// Test branch conditions:
// - No height.
// - Upscale, don't need to upscale.
$tests[] = [
'input' => [
'dimensions' => [
'width' => 1000,
'height' => 2000,
],
'width' => 200,
'height' => NULL,
'upscale' => TRUE,
],
'output' => [
'dimensions' => [
'width' => 200,
'height' => 400,
],
'return_value' => TRUE,
],
];
// Test branch conditions:
// - No width.
// - Don't upscale, don't need to upscale.
$tests[] = [
'input' => [
'dimensions' => [
'width' => 1000,
'height' => 800,
],
'width' => NULL,
'height' => 140,
'upscale' => FALSE,
],
'output' => [
'dimensions' => [
'width' => 175,
'height' => 140,
],
'return_value' => TRUE,
],
];
// Test branch conditions:
// - Source aspect ratio greater than target.
// - Upscale, need to upscale.
$tests[] = [
'input' => [
'dimensions' => [
'width' => 8,
'height' => 20,
],
'width' => 200,
'height' => 140,
'upscale' => TRUE,
],
'output' => [
'dimensions' => [
'width' => 56,
'height' => 140,
],
'return_value' => TRUE,
],
];
// Test branch condition: target aspect ratio greater than source.
$tests[] = [
'input' => [
'dimensions' => [
'width' => 2000,
'height' => 800,
],
'width' => 200,
'height' => 140,
'upscale' => FALSE,
],
'output' => [
'dimensions' => [
'width' => 200,
'height' => 80,
],
'return_value' => TRUE,
],
];
// Test branch condition: don't upscale, need to upscale.
$tests[] = [
'input' => [
'dimensions' => [
'width' => 100,
'height' => 50,
],
'width' => 200,
'height' => 140,
'upscale' => FALSE,
],
'output' => [
'dimensions' => [
'width' => 100,
'height' => 50,
],
'return_value' => FALSE,
],
];
return $tests;
}
}
@@ -0,0 +1,283 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\NestedArray;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\Utility\NestedArray
* @group Utility
*/
class NestedArrayTest extends UnitTestCase {
/**
* Form array to check.
*
* @var array
*/
protected $form;
/**
* Array of parents for the nested element.
*
* @var array
*/
protected $parents;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a form structure with a nested element.
$this->form['details']['element'] = [
'#value' => 'Nested element',
];
// Set up parent array.
$this->parents = ['details', 'element'];
}
/**
* Tests getting nested array values.
*
* @covers ::getValue
*/
public function testGetValue() {
// Verify getting a value of a nested element.
$value = NestedArray::getValue($this->form, $this->parents);
$this->assertSame('Nested element', $value['#value'], 'Nested element value found.');
// Verify changing a value of a nested element by reference.
$value = &NestedArray::getValue($this->form, $this->parents);
$value['#value'] = 'New value';
$value = NestedArray::getValue($this->form, $this->parents);
$this->assertSame('New value', $value['#value'], 'Nested element value was changed by reference.');
$this->assertSame('New value', $this->form['details']['element']['#value'], 'Nested element value was changed by reference.');
// Verify that an existing key is reported back.
$key_exists = NULL;
NestedArray::getValue($this->form, $this->parents, $key_exists);
$this->assertTrue($key_exists, 'Existing key found.');
// Verify that a non-existing key is reported back and throws no errors.
$key_exists = NULL;
$parents = $this->parents;
$parents[] = 'foo';
NestedArray::getValue($this->form, $parents, $key_exists);
$this->assertFalse($key_exists, 'Non-existing key not found.');
}
/**
* Tests setting nested array values.
*
* @covers ::setValue
*/
public function testSetValue() {
$new_value = [
'#value' => 'New value',
'#required' => TRUE,
];
// Verify setting the value of a nested element.
NestedArray::setValue($this->form, $this->parents, $new_value);
$this->assertSame('New value', $this->form['details']['element']['#value'], 'Changed nested element value found.');
$this->assertTrue($this->form['details']['element']['#required'], 'New nested element value found.');
}
/**
* Tests force-setting values.
*
* @covers ::setValue
*/
public function testSetValueForce() {
$new_value = [
'one',
];
$this->form['details']['non-array-parent'] = 'string';
$parents = ['details', 'non-array-parent', 'child'];
NestedArray::setValue($this->form, $parents, $new_value, TRUE);
$this->assertSame($new_value, $this->form['details']['non-array-parent']['child'], 'The nested element was not forced to the new value.');
}
/**
* Tests unsetting nested array values.
*
* @covers ::unsetValue
*/
public function testUnsetValue() {
// Verify unsetting a non-existing nested element throws no errors and the
// non-existing key is properly reported.
$key_existed = NULL;
$parents = $this->parents;
$parents[] = 'foo';
NestedArray::unsetValue($this->form, $parents, $key_existed);
$this->assertTrue(isset($this->form['details']['element']['#value']), 'Outermost nested element key still exists.');
$this->assertFalse($key_existed, 'Non-existing key not found.');
// Verify unsetting a nested element.
$key_existed = NULL;
NestedArray::unsetValue($this->form, $this->parents, $key_existed);
$this->assertFalse(isset($this->form['details']['element']), 'Removed nested element not found.');
$this->assertTrue($key_existed, 'Existing key was found.');
}
/**
* Tests existence of array key.
*/
public function testKeyExists() {
// Verify that existing key is found.
$this->assertTrue(NestedArray::keyExists($this->form, $this->parents), 'Nested key found.');
// Verify that non-existing keys are not found.
$parents = $this->parents;
$parents[] = 'foo';
$this->assertFalse(NestedArray::keyExists($this->form, $parents), 'Non-existing nested key not found.');
}
/**
* Tests NestedArray::mergeDeepArray().
*
* @covers ::mergeDeep
* @covers ::mergeDeepArray
*/
public function testMergeDeepArray() {
$link_options_1 = [
'fragment' => 'x',
'attributes' => ['title' => 'X', 'class' => ['a', 'b']],
'language' => 'en',
];
$link_options_2 = [
'fragment' => 'y',
'attributes' => ['title' => 'Y', 'class' => ['c', 'd']],
'absolute' => TRUE,
];
$expected = [
'fragment' => 'y',
'attributes' => ['title' => 'Y', 'class' => ['a', 'b', 'c', 'd']],
'language' => 'en',
'absolute' => TRUE,
];
$this->assertSame($expected, NestedArray::mergeDeepArray([$link_options_1, $link_options_2]), 'NestedArray::mergeDeepArray() returned a properly merged array.');
// Test wrapper function, NestedArray::mergeDeep().
$this->assertSame($expected, NestedArray::mergeDeep($link_options_1, $link_options_2), 'NestedArray::mergeDeep() returned a properly merged array.');
}
/**
* Tests that arrays with implicit keys are appended, not merged.
*
* @covers ::mergeDeepArray
*/
public function testMergeImplicitKeys() {
$a = [
'subkey' => ['X', 'Y'],
];
$b = [
'subkey' => ['X'],
];
// Drupal core behavior.
$expected = [
'subkey' => ['X', 'Y', 'X'],
];
$actual = NestedArray::mergeDeepArray([$a, $b]);
$this->assertSame($expected, $actual, 'drupal_array_merge_deep() creates new numeric keys in the implicit sequence.');
}
/**
* Tests that even with explicit keys, values are appended, not merged.
*
* @covers ::mergeDeepArray
*/
public function testMergeExplicitKeys() {
$a = [
'subkey' => [
0 => 'A',
1 => 'B',
],
];
$b = [
'subkey' => [
0 => 'C',
1 => 'D',
],
];
// Drupal core behavior.
$expected = [
'subkey' => [
0 => 'A',
1 => 'B',
2 => 'C',
3 => 'D',
],
];
$actual = NestedArray::mergeDeepArray([$a, $b]);
$this->assertSame($expected, $actual, 'drupal_array_merge_deep() creates new numeric keys in the explicit sequence.');
}
/**
* Tests that array keys values on the first array are ignored when merging.
*
* Even if the initial ordering would place the data from the second array
* before those in the first one, they are still appended, and the keys on
* the first array are deleted and regenerated.
*
* @covers ::mergeDeepArray
*/
public function testMergeOutOfSequenceKeys() {
$a = [
'subkey' => [
10 => 'A',
30 => 'B',
],
];
$b = [
'subkey' => [
20 => 'C',
0 => 'D',
],
];
// Drupal core behavior.
$expected = [
'subkey' => [
0 => 'A',
1 => 'B',
2 => 'C',
3 => 'D',
],
];
$actual = NestedArray::mergeDeepArray([$a, $b]);
$this->assertSame($expected, $actual, 'drupal_array_merge_deep() ignores numeric key order when merging.');
}
/**
* @covers ::filter
* @dataProvider providerTestFilter
*/
public function testFilter($array, $callable, $expected) {
$this->assertEquals($expected, NestedArray::filter($array, $callable));
}
public function providerTestFilter() {
$data = [];
$data['1d-array'] = [
[0, 1, '', TRUE], NULL, [1 => 1, 3 => TRUE]
];
$data['1d-array-callable'] = [
[0, 1, '', TRUE], function ($element) { return $element === ''; }, [2 => '']
];
$data['2d-array'] = [
[[0, 1, '', TRUE], [0, 1, 2, 3]], NULL, [0 => [1 => 1, 3 => TRUE], 1 => [1 => 1, 2 => 2, 3 => 3]],
];
$data['2d-array-callable'] = [
[[0, 1, '', TRUE], [0, 1, 2, 3]], function ($element) { return is_array($element) || $element === 3; }, [0 => [], 1 => [3 => 3]],
];
return $data;
}
}
@@ -0,0 +1,158 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Number;
use Drupal\Tests\UnitTestCase;
/**
* Tests number manipulation utilities.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Number
*
* @see \Drupal\Component\Utility\Number
*/
class NumberTest extends UnitTestCase {
/**
* Tests Number::validStep() without offset.
*
* @dataProvider providerTestValidStep
* @covers ::validStep
*
* @param numeric $value
* The value argument for Number::validStep().
* @param numeric $step
* The step argument for Number::validStep().
* @param bool $expected
* Expected return value from Number::validStep().
*/
public function testValidStep($value, $step, $expected) {
$return = Number::validStep($value, $step);
$this->assertEquals($expected, $return);
}
/**
* Tests Number::validStep() with offset.
*
* @dataProvider providerTestValidStepOffset
* @covers ::validStep
*
* @param numeric $value
* The value argument for Number::validStep().
* @param numeric $step
* The step argument for Number::validStep().
* @param numeric $offset
* The offset argument for Number::validStep().
* @param bool $expected
* Expected return value from Number::validStep().
*/
public function testValidStepOffset($value, $step, $offset, $expected) {
$return = Number::validStep($value, $step, $offset);
$this->assertEquals($expected, $return);
}
/**
* Provides data for self::testNumberStep().
*
* @see \Drupal\Tests\Component\Utility\Number::testValidStep
*/
public static function providerTestValidStep() {
return [
// Value and step equal.
[10.3, 10.3, TRUE],
// Valid integer steps.
[42, 21, TRUE],
[42, 3, TRUE],
// Valid float steps.
[42, 10.5, TRUE],
[1, 1 / 3, TRUE],
[-100, 100 / 7, TRUE],
[1000, -10, TRUE],
// Valid and very small float steps.
[1000.12345, 1e-10, TRUE],
[3.9999999999999, 1e-13, TRUE],
// Invalid integer steps.
[100, 30, FALSE],
[-10, 4, FALSE],
// Invalid float steps.
[6, 5 / 7, FALSE],
[10.3, 10.25, FALSE],
// Step mismatches very close to being valid.
[70 + 9e-7, 10 + 9e-7, FALSE],
[1936.5, 3e-8, FALSE],
];
}
/**
* Data provider for \Drupal\Test\Component\Utility\NumberTest::testValidStepOffset().
*
* @see \Drupal\Test\Component\Utility\NumberTest::testValidStepOffset()
*/
public static function providerTestValidStepOffset() {
return [
// Try obvious fits.
[11.3, 10.3, 1, TRUE],
[100, 10, 50, TRUE],
[-100, 90 / 7, -10, TRUE],
[2 / 7 + 5 / 9, 1 / 7, 5 / 9, TRUE],
// Ensure a small offset is still invalid.
[10.3, 10.3, 0.0001, FALSE],
[1 / 5, 1 / 7, 1 / 11, FALSE],
// Try negative values and offsets.
[1000, 10, -5, FALSE],
[-10, 4, 0, FALSE],
[-10, 4, -4, FALSE],
];
}
/**
* Tests the alphadecimal conversion functions.
*
* @dataProvider providerTestConversions
* @covers ::intToAlphadecimal
* @covers ::alphadecimalToInt
*
* @param int $value
* The integer value.
* @param string $expected
* The expected alphadecimal value.
*/
public function testConversions($value, $expected) {
$this->assertSame(Number::intToAlphadecimal($value), $expected);
$this->assertSame($value, Number::alphadecimalToInt($expected));
}
/**
* Data provider for testConversions().
*
* @see testConversions()
*
* @return array
* An array containing:
* - The integer value.
* - The alphadecimal value.
*/
public function providerTestConversions() {
return [
[0, '00'],
[1, '01'],
[10, '0a'],
[20, '0k'],
[35, '0z'],
[36, '110'],
[100, '12s'],
];
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Random;
use Drupal\Tests\UnitTestCase;
/**
* Tests random data generation.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Random
*/
class RandomTest extends UnitTestCase {
/**
* The first random string passed to the test callback.
*
* @see \Drupal\Tests\Component\Utility\RandomTest::_RandomStringValidate()
*
* @var string
*/
protected $firstStringGenerated = '';
/**
* Tests unique random string generation.
*
* @covers ::string
*/
public function testRandomStringUniqueness() {
$strings = [];
$random = new Random();
for ($i = 0; $i <= 50; $i++) {
$str = $random->string(1, TRUE);
$this->assertFalse(isset($strings[$str]), 'Generated duplicate random string ' . $str);
$strings[$str] = TRUE;
}
}
/**
* Tests unique random name generation.
*
* @covers ::name
*/
public function testRandomNamesUniqueness() {
$names = [];
$random = new Random();
for ($i = 0; $i <= 10; $i++) {
$str = $random->name(1, TRUE);
$this->assertFalse(isset($names[$str]), 'Generated duplicate random name ' . $str);
$names[$str] = TRUE;
}
}
/**
* Tests infinite loop prevention whilst generating random names.
*
* @covers ::name
*/
public function testRandomNameException() {
// There are fewer than 100 possibilities so an exception should occur to
// prevent infinite loops.
$random = new Random();
$this->setExpectedException(\RuntimeException::class);
for ($i = 0; $i <= 100; $i++) {
$str = $random->name(1, TRUE);
$names[$str] = TRUE;
}
}
/**
* Tests infinite loop prevention whilst generating random strings.
*
* @covers ::string
*/
public function testRandomStringException() {
// There are fewer than 100 possibilities so an exception should occur to
// prevent infinite loops.
$random = new Random();
$this->setExpectedException(\RuntimeException::class);
for ($i = 0; $i <= 100; $i++) {
$str = $random->string(1, TRUE);
$names[$str] = TRUE;
}
}
/**
* Tests random name generation if uniqueness is not enforced.
*
* @covers ::name
*/
public function testRandomNameNonUnique() {
// There are fewer than 100 possibilities if we were forcing uniqueness so
// exception would occur.
$random = new Random();
for ($i = 0; $i <= 100; $i++) {
$random->name(1);
}
$this->assertTrue(TRUE, 'No exception thrown when uniqueness is not enforced.');
}
/**
* Tests random string if uniqueness is not enforced.
*
* @covers ::string
*/
public function testRandomStringNonUnique() {
// There are fewer than 100 possibilities if we were forcing uniqueness so
// exception would occur.
$random = new Random();
for ($i = 0; $i <= 100; $i++) {
$random->string(1);
}
$this->assertTrue(TRUE, 'No exception thrown when uniqueness is not enforced.');
}
/**
* Tests random object generation to ensure the expected number of properties.
*
* @covers ::object
*/
public function testRandomObject() {
// For values of 0 and 1 \Drupal\Component\Utility\Random::object() will
// have different execution paths.
$random = new Random();
for ($i = 0; $i <= 1; $i++) {
$obj = $random->object($i);
$this->assertEquals($i, count(get_object_vars($obj)), 'Generated random object has expected number of properties');
}
}
/**
* Tests random string validation callbacks.
*
* @covers ::string
*/
public function testRandomStringValidator() {
$random = new Random();
$this->firstStringGenerated = '';
$str = $random->string(1, TRUE, [$this, '_RandomStringValidate']);
$this->assertNotEquals($this->firstStringGenerated, $str);
}
/**
* Callback for random string validation.
*
* @see \Drupal\Component\Utility\Random::name()
* @see \Drupal\Tests\Component\Utility\RandomTest::testRandomStringValidator()
*
* @param string $string
* The random string to validate.
*
* @return bool
* TRUE if the random string is valid, FALSE if not.
*/
public function _RandomStringValidate($string) {
// Return FALSE for the first generated string and any string that is the
// same, as the test expects a different string to be returned.
if (empty($this->firstStringGenerated) || $string == $this->firstStringGenerated) {
$this->firstStringGenerated = $string;
return FALSE;
}
return TRUE;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Utility\SafeMarkupTest.
*/
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Render\HtmlEscapedText;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\MarkupTrait;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Tests\UnitTestCase;
/**
* Tests marking strings as safe.
*
* @group Utility
* @coversDefaultClass \Drupal\Component\Utility\SafeMarkup
*/
class SafeMarkupTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function tearDown() {
parent::tearDown();
UrlHelper::setAllowedProtocols(['http', 'https']);
}
/**
* Tests SafeMarkup::isSafe() with different objects.
*
* @covers ::isSafe
*/
public function testIsSafe() {
$safe_string = $this->getMock('\Drupal\Component\Render\MarkupInterface');
$this->assertTrue(SafeMarkup::isSafe($safe_string));
$string_object = new SafeMarkupTestString('test');
$this->assertFalse(SafeMarkup::isSafe($string_object));
}
/**
* Tests SafeMarkup::checkPlain().
*
* @dataProvider providerCheckPlain
* @covers ::checkPlain
*
* @param string $text
* The text to provide to SafeMarkup::checkPlain().
* @param string $expected
* The expected output from the function.
* @param string $message
* The message to provide as output for the test.
*/
public function testCheckPlain($text, $expected, $message) {
$result = SafeMarkup::checkPlain($text);
$this->assertTrue($result instanceof HtmlEscapedText);
$this->assertEquals($expected, $result, $message);
}
/**
* Tests Drupal\Component\Render\HtmlEscapedText.
*
* Verifies that the result of SafeMarkup::checkPlain() is the same as using
* HtmlEscapedText directly.
*
* @dataProvider providerCheckPlain
*
* @param string $text
* The text to provide to the HtmlEscapedText constructor.
* @param string $expected
* The expected output from the function.
* @param string $message
* The message to provide as output for the test.
*/
public function testHtmlEscapedText($text, $expected, $message) {
$result = new HtmlEscapedText($text);
$this->assertEquals($expected, $result, $message);
}
/**
* Data provider for testCheckPlain() and testEscapeString().
*
* @see testCheckPlain()
*/
public function providerCheckPlain() {
// Checks that invalid multi-byte sequences are escaped.
$tests[] = ["Foo\xC0barbaz", 'Foobarbaz', 'Escapes invalid sequence "Foo\xC0barbaz"'];
$tests[] = ["\xc2\"", '&quot;', 'Escapes invalid sequence "\xc2\""'];
$tests[] = ["Fooÿñ", "Fooÿñ", 'Does not escape valid sequence "Fooÿñ"'];
// Checks that special characters are escaped.
$tests[] = [SafeMarkupTestMarkup::create("<script>"), '&lt;script&gt;', 'Escapes &lt;script&gt; even inside an object that implements MarkupInterface.'];
$tests[] = ["<script>", '&lt;script&gt;', 'Escapes &lt;script&gt;'];
$tests[] = ['<>&"\'', '&lt;&gt;&amp;&quot;&#039;', 'Escapes reserved HTML characters.'];
$tests[] = [SafeMarkupTestMarkup::create('<>&"\''), '&lt;&gt;&amp;&quot;&#039;', 'Escapes reserved HTML characters even inside an object that implements MarkupInterface.'];
return $tests;
}
/**
* Tests string formatting with SafeMarkup::format().
*
* @dataProvider providerFormat
* @covers ::format
*
* @param string $string
* The string to run through SafeMarkup::format().
* @param string[] $args
* The arguments to pass into SafeMarkup::format().
* @param string $expected
* The expected result from calling the function.
* @param string $message
* The message to display as output to the test.
* @param bool $expected_is_safe
* Whether the result is expected to be safe for HTML display.
*/
public function testFormat($string, array $args, $expected, $message, $expected_is_safe) {
UrlHelper::setAllowedProtocols(['http', 'https', 'mailto']);
$result = SafeMarkup::format($string, $args);
$this->assertEquals($expected, (string) $result, $message);
$this->assertEquals($expected_is_safe, $result instanceof MarkupInterface, 'SafeMarkup::format correctly sets the result as safe or not safe.');
foreach ($args as $arg) {
$this->assertSame($arg instanceof SafeMarkupTestMarkup, SafeMarkup::isSafe($arg));
}
}
/**
* Data provider for testFormat().
*
* @see testFormat()
*/
public function providerFormat() {
$tests[] = ['Simple text', [], 'Simple text', 'SafeMarkup::format leaves simple text alone.', TRUE];
$tests[] = ['Escaped text: @value', ['@value' => '<script>'], 'Escaped text: &lt;script&gt;', 'SafeMarkup::format replaces and escapes string.', TRUE];
$tests[] = ['Escaped text: @value', ['@value' => SafeMarkupTestMarkup::create('<span>Safe HTML</span>')], 'Escaped text: <span>Safe HTML</span>', 'SafeMarkup::format does not escape an already safe string.', TRUE];
$tests[] = ['Placeholder text: %value', ['%value' => '<script>'], 'Placeholder text: <em class="placeholder">&lt;script&gt;</em>', 'SafeMarkup::format replaces, escapes and themes string.', TRUE];
$tests[] = ['Placeholder text: %value', ['%value' => SafeMarkupTestMarkup::create('<span>Safe HTML</span>')], 'Placeholder text: <em class="placeholder"><span>Safe HTML</span></em>', 'SafeMarkup::format does not escape an already safe string themed as a placeholder.', TRUE];
$tests['javascript-protocol-url'] = ['Simple text <a href=":url">giraffe</a>', [':url' => 'javascript://example.com?foo&bar'], 'Simple text <a href="//example.com?foo&amp;bar">giraffe</a>', 'Support for filtering bad protocols', TRUE];
$tests['external-url'] = ['Simple text <a href=":url">giraffe</a>', [':url' => 'http://example.com?foo&bar'], 'Simple text <a href="http://example.com?foo&amp;bar">giraffe</a>', 'Support for filtering bad protocols', TRUE];
$tests['relative-url'] = ['Simple text <a href=":url">giraffe</a>', [':url' => '/node/1?foo&bar'], 'Simple text <a href="/node/1?foo&amp;bar">giraffe</a>', 'Support for filtering bad protocols', TRUE];
$tests['fragment-with-special-chars'] = ['Simple text <a href=":url">giraffe</a>', [':url' => 'http://example.com/#&lt;'], 'Simple text <a href="http://example.com/#&amp;lt;">giraffe</a>', 'Support for filtering bad protocols', TRUE];
$tests['mailto-protocol'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => 'mailto:test@example.com'], 'Hey giraffe <a href="mailto:test@example.com">MUUUH</a>', '', TRUE];
$tests['js-with-fromCharCode'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "javascript:alert(String.fromCharCode(88,83,83))"], 'Hey giraffe <a href="alert(String.fromCharCode(88,83,83))">MUUUH</a>', '', TRUE];
// Test some "URL" values that are not RFC 3986 compliant URLs. The result
// of SafeMarkup::format() should still be valid HTML (other than the
// value of the "href" attribute not being a valid URL), and not
// vulnerable to XSS.
$tests['non-url-with-colon'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "llamas: they are not URLs"], 'Hey giraffe <a href=" they are not URLs">MUUUH</a>', '', TRUE];
$tests['non-url-with-html'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "<span>not a url</span>"], 'Hey giraffe <a href="&lt;span&gt;not a url&lt;/span&gt;">MUUUH</a>', '', TRUE];
// Tests non-standard placeholders that will not replace.
$tests['non-standard-placeholder'] = ['Hey hey', ['risky' => "<script>alert('foo');</script>"], 'Hey hey', '', TRUE];
return $tests;
}
}
class SafeMarkupTestString {
protected $string;
public function __construct($string) {
$this->string = $string;
}
public function __toString() {
return $this->string;
}
}
/**
* Marks an object's __toString() method as returning markup.
*/
class SafeMarkupTestMarkup implements MarkupInterface {
use MarkupTrait;
}
@@ -0,0 +1,323 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\SortArray;
/**
* Tests the SortArray component.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\SortArray
*/
class SortArrayTest extends UnitTestCase {
/**
* Tests SortArray::sortByWeightElement() input against expected output.
*
* @dataProvider providerSortByWeightElement
* @covers ::sortByWeightElement
* @covers ::sortByKeyInt
*
* @param array $a
* The first input array for the SortArray::sortByWeightElement() method.
* @param array $b
* The second input array for the SortArray::sortByWeightElement().
* @param int $expected
* The expected output from calling the method.
*/
public function testSortByWeightElement($a, $b, $expected) {
$result = SortArray::sortByWeightElement($a, $b);
$this->assertBothNegativePositiveOrZero($expected, $result);
}
/**
* Data provider for SortArray::sortByWeightElement().
*
* @return array
* An array of tests, matching the parameter inputs for
* testSortByWeightElement.
*
* @see \Drupal\Tests\Component\Utility\SortArrayTest::testSortByWeightElement()
*/
public function providerSortByWeightElement() {
$tests = [];
// Weights set and equal.
$tests[] = [
['weight' => 1],
['weight' => 1],
0
];
// Weights set and $a is less (lighter) than $b.
$tests[] = [
['weight' => 1],
['weight' => 2],
-1
];
// Weights set and $a is greater (heavier) than $b.
$tests[] = [
['weight' => 2],
['weight' => 1],
1
];
// Weights not set.
$tests[] = [
[],
[],
0
];
// Weights for $b not set.
$tests[] = [
['weight' => 1],
[],
1
];
// Weights for $a not set.
$tests[] = [
[],
['weight' => 1],
-1
];
return $tests;
}
/**
* Tests SortArray::sortByWeightProperty() input against expected output.
*
* @dataProvider providerSortByWeightProperty
* @covers ::sortByWeightProperty
* @covers ::sortByKeyInt
*
* @param array $a
* The first input array for the SortArray::sortByWeightProperty() method.
* @param array $b
* The second input array for the SortArray::sortByWeightProperty().
* @param int $expected
* The expected output from calling the method.
*/
public function testSortByWeightProperty($a, $b, $expected) {
$result = SortArray::sortByWeightProperty($a, $b);
$this->assertBothNegativePositiveOrZero($expected, $result);
}
/**
* Data provider for SortArray::sortByWeightProperty().
*
* @return array
* An array of tests, matching the parameter inputs for
* testSortByWeightProperty.
*
* @see \Drupal\Tests\Component\Utility\SortArrayTest::testSortByWeightProperty()
*/
public function providerSortByWeightProperty() {
$tests = [];
// Weights set and equal.
$tests[] = [
['#weight' => 1],
['#weight' => 1],
0
];
// Weights set and $a is less (lighter) than $b.
$tests[] = [
['#weight' => 1],
['#weight' => 2],
-1
];
// Weights set and $a is greater (heavier) than $b.
$tests[] = [
['#weight' => 2],
['#weight' => 1],
1
];
// Weights not set.
$tests[] = [
[],
[],
0
];
// Weights for $b not set.
$tests[] = [
['#weight' => 1],
[],
1
];
// Weights for $a not set.
$tests[] = [
[],
['#weight' => 1],
-1
];
return $tests;
}
/**
* Tests SortArray::sortByTitleElement() input against expected output.
*
* @dataProvider providerSortByTitleElement
* @covers ::sortByTitleElement
* @covers ::sortByKeyString
*
* @param array $a
* The first input item for comparison.
* @param array $b
* The second item for comparison.
* @param int $expected
* The expected output from calling the method.
*/
public function testSortByTitleElement($a, $b, $expected) {
$result = SortArray::sortByTitleElement($a, $b);
$this->assertBothNegativePositiveOrZero($expected, $result);
}
/**
* Data provider for SortArray::sortByTitleElement().
*
* @return array
* An array of tests, matching the parameter inputs for
* testSortByTitleElement.
*
* @see \Drupal\Tests\Component\Utility\SortArrayTest::testSortByTitleElement()
*/
public function providerSortByTitleElement() {
$tests = [];
// Titles set and equal.
$tests[] = [
['title' => 'test'],
['title' => 'test'],
0
];
// Title $a not set.
$tests[] = [
[],
['title' => 'test'],
-4
];
// Title $b not set.
$tests[] = [
['title' => 'test'],
[],
4
];
// Titles set but not equal.
$tests[] = [
['title' => 'test'],
['title' => 'testing'],
-1
];
// Titles set but not equal.
$tests[] = [
['title' => 'testing'],
['title' => 'test'],
1
];
return $tests;
}
/**
* Tests SortArray::sortByTitleProperty() input against expected output.
*
* @dataProvider providerSortByTitleProperty
* @covers ::sortByTitleProperty
* @covers ::sortByKeyString
*
* @param array $a
* The first input item for comparison.
* @param array $b
* The second item for comparison.
* @param int $expected
* The expected output from calling the method.
*/
public function testSortByTitleProperty($a, $b, $expected) {
$result = SortArray::sortByTitleProperty($a, $b);
$this->assertBothNegativePositiveOrZero($expected, $result);
}
/**
* Data provider for SortArray::sortByTitleProperty().
*
* @return array
* An array of tests, matching the parameter inputs for
* testSortByTitleProperty.
*
* @see \Drupal\Tests\Component\Utility\SortArrayTest::testSortByTitleProperty()
*/
public function providerSortByTitleProperty() {
$tests = [];
// Titles set and equal.
$tests[] = [
['#title' => 'test'],
['#title' => 'test'],
0
];
// Title $a not set.
$tests[] = [
[],
['#title' => 'test'],
-4
];
// Title $b not set.
$tests[] = [
['#title' => 'test'],
[],
4
];
// Titles set but not equal.
$tests[] = [
['#title' => 'test'],
['#title' => 'testing'],
-1
];
// Titles set but not equal.
$tests[] = [
['#title' => 'testing'],
['#title' => 'test'],
1
];
return $tests;
}
/**
* Asserts that numbers are either both negative, both positive or both zero.
*
* The exact values returned by comparison functions differ between PHP
* versions and are considered an "implementation detail".
*
* @param int $expected
* Expected comparison function return value.
* @param int $result
* Actual comparison function return value.
*/
protected function assertBothNegativePositiveOrZero($expected, $result) {
$this->assertTrue(is_numeric($expected) && is_numeric($result), 'Parameters are numeric.');
$this->assertTrue(($expected < 0 && $result < 0) || ($expected > 0 && $result > 0) || ($expected === 0 && $result === 0), 'Numbers are either both negative, both positive or both zero.');
}
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\Component\Utility;
/**
* Used by SafeMarkupTest to test that a class with a __toString() method works.
*/
class TextWrapper {
/**
* The text value.
*
* @var string
*/
protected $text = '';
/**
* Constructs a \Drupal\Tests\Component\Utility\TextWrapper
*
* @param string $text
*/
public function __construct($text) {
$this->text = $text;
}
/**
* Magic method
*
* @return string
*/
public function __toString() {
return $this->text;
}
}
@@ -0,0 +1,68 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\Timer;
/**
* Tests the Timer system.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Timer
*/
class TimerTest extends UnitTestCase {
/**
* Tests Timer::read() time accumulation accuracy across multiple restarts.
*
* @covers ::start
* @covers ::stop
* @covers ::read
*/
public function testTimer() {
Timer::start('test');
usleep(5000);
$value = Timer::read('test');
usleep(5000);
$value2 = Timer::read('test');
usleep(5000);
$value3 = Timer::read('test');
usleep(5000);
$value4 = Timer::read('test');
// Although we sleep for 5 milliseconds, we should test that at least 4 ms
// have past because usleep() is not reliable on Windows. See
// http://php.net/manual/function.usleep.php for more information. The
// purpose of the test to validate that the Timer class can measure elapsed
// time not the granularity of usleep() on a particular OS.
$this->assertGreaterThanOrEqual(4, $value, 'Timer failed to measure at least 4 milliseconds of sleeping while running.');
$this->assertGreaterThanOrEqual($value + 4, $value2, 'Timer failed to measure at least 8 milliseconds of sleeping while running.');
$this->assertGreaterThanOrEqual($value2 + 4, $value3, 'Timer failed to measure at least 12 milliseconds of sleeping while running.');
$this->assertGreaterThanOrEqual($value3 + 4, $value4, 'Timer failed to measure at least 16 milliseconds of sleeping while running.');
// Stop the timer.
$value5 = Timer::stop('test');
$this->assertGreaterThanOrEqual($value4, $value5['time'], 'Timer measured after stopping was not greater than last measurement.');
// Read again.
$value6 = Timer::read('test');
$this->assertEquals($value5['time'], $value6, 'Timer measured after stopping was not equal to the stopped time.');
// Restart.
Timer::start('test');
usleep(5000);
$value7 = Timer::read('test');
$this->assertGreaterThanOrEqual($value6 + 4, $value7, 'Timer failed to measure at least 16 milliseconds of sleeping while running.');
// Stop again.
$value8 = Timer::stop('test');
$value9 = Timer::read('test');
$this->assertEquals($value8['time'], $value9, 'Timer measured after stopping not equal to stop time.');
}
}
@@ -0,0 +1,570 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\Unicode;
/**
* Test unicode handling features implemented in Unicode component.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Unicode
*/
class UnicodeTest extends UnitTestCase {
/**
* {@inheritdoc}
*
* @covers ::check
*/
protected function setUp() {
// Initialize unicode component.
Unicode::check();
}
/**
* Getting and settings the multibyte environment status.
*
* @dataProvider providerTestStatus
* @covers ::getStatus
* @covers ::setStatus
*/
public function testStatus($value, $expected, $invalid = FALSE) {
if ($invalid) {
$this->setExpectedException('InvalidArgumentException');
}
Unicode::setStatus($value);
$this->assertEquals($expected, Unicode::getStatus());
}
/**
* Data provider for testStatus().
*
* @see testStatus()
*
* @return array
* An array containing:
* - The status value to set.
* - The status value to expect after setting the new value.
* - (optional) Boolean indicating invalid status. Defaults to FALSE.
*/
public function providerTestStatus() {
return [
[Unicode::STATUS_SINGLEBYTE, Unicode::STATUS_SINGLEBYTE],
[rand(10, 100), Unicode::STATUS_SINGLEBYTE, TRUE],
[rand(10, 100), Unicode::STATUS_SINGLEBYTE, TRUE],
[Unicode::STATUS_MULTIBYTE, Unicode::STATUS_MULTIBYTE],
[rand(10, 100), Unicode::STATUS_MULTIBYTE, TRUE],
[Unicode::STATUS_ERROR, Unicode::STATUS_ERROR],
[Unicode::STATUS_MULTIBYTE, Unicode::STATUS_MULTIBYTE],
];
}
/**
* Tests multibyte encoding and decoding.
*
* @dataProvider providerTestMimeHeader
* @covers ::mimeHeaderEncode
* @covers ::mimeHeaderDecode
*/
public function testMimeHeader($value, $encoded) {
$this->assertEquals($encoded, Unicode::mimeHeaderEncode($value));
$this->assertEquals($value, Unicode::mimeHeaderDecode($encoded));
}
/**
* Data provider for testMimeHeader().
*
* @see testMimeHeader()
*
* @return array
* An array containing a string and its encoded value.
*/
public function providerTestMimeHeader() {
return [
['tést.txt', '=?UTF-8?B?dMOpc3QudHh0?='],
// Simple ASCII characters.
['ASCII', 'ASCII'],
];
}
/**
* Tests multibyte strtolower.
*
* @dataProvider providerStrtolower
* @covers ::strtolower
* @covers ::caseFlip
*/
public function testStrtolower($text, $expected, $multibyte = FALSE) {
$status = $multibyte ? Unicode::STATUS_MULTIBYTE : Unicode::STATUS_SINGLEBYTE;
Unicode::setStatus($status);
$this->assertEquals($expected, Unicode::strtolower($text));
}
/**
* Data provider for testStrtolower().
*
* @see testStrtolower()
*
* @return array
* An array containing a string, its lowercase version and whether it should
* be processed as multibyte.
*/
public function providerStrtolower() {
$cases = [
['tHe QUIcK bRoWn', 'the quick brown'],
['FrançAIS is ÜBER-åwesome', 'français is über-åwesome'],
];
foreach ($cases as $case) {
// Test the same string both in multibyte and singlebyte conditions.
array_push($case, TRUE);
$cases[] = $case;
}
// Add a multibyte string.
$cases[] = ['ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ', 'αβγδεζηθικλμνξοσὠ', TRUE];
return $cases;
}
/**
* Tests multibyte strtoupper.
*
* @dataProvider providerStrtoupper
* @covers ::strtoupper
* @covers ::caseFlip
*/
public function testStrtoupper($text, $expected, $multibyte = FALSE) {
$status = $multibyte ? Unicode::STATUS_MULTIBYTE : Unicode::STATUS_SINGLEBYTE;
Unicode::setStatus($status);
$this->assertEquals($expected, Unicode::strtoupper($text));
}
/**
* Data provider for testStrtoupper().
*
* @see testStrtoupper()
*
* @return array
* An array containing a string, its uppercase version and whether it should
* be processed as multibyte.
*/
public function providerStrtoupper() {
$cases = [
['tHe QUIcK bRoWn', 'THE QUICK BROWN'],
['FrançAIS is ÜBER-åwesome', 'FRANÇAIS IS ÜBER-ÅWESOME'],
];
foreach ($cases as $case) {
// Test the same string both in multibyte and singlebyte conditions.
array_push($case, TRUE);
$cases[] = $case;
}
// Add a multibyte string.
$cases[] = ['αβγδεζηθικλμνξοσὠ', 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ', TRUE];
return $cases;
}
/**
* Tests multibyte ucfirst.
*
* @dataProvider providerUcfirst
* @covers ::ucfirst
*/
public function testUcfirst($text, $expected) {
$this->assertEquals($expected, Unicode::ucfirst($text));
}
/**
* Data provider for testUcfirst().
*
* @see testUcfirst()
*
* @return array
* An array containing a string and its uppercase first version.
*/
public function providerUcfirst() {
return [
['tHe QUIcK bRoWn', 'THe QUIcK bRoWn'],
['françAIS', 'FrançAIS'],
['über', 'Über'],
['åwesome', 'Åwesome'],
// A multibyte string.
['σion', 'Σion'],
];
}
/**
* Tests multibyte lcfirst.
*
* @dataProvider providerLcfirst
* @covers ::lcfirst
*/
public function testLcfirst($text, $expected, $multibyte = FALSE) {
$status = $multibyte ? Unicode::STATUS_MULTIBYTE : Unicode::STATUS_SINGLEBYTE;
Unicode::setStatus($status);
$this->assertEquals($expected, Unicode::lcfirst($text));
}
/**
* Data provider for testLcfirst().
*
* @see testLcfirst()
*
* @return array
* An array containing a string, its lowercase version and whether it should
* be processed as multibyte.
*/
public function providerLcfirst() {
return [
['tHe QUIcK bRoWn', 'tHe QUIcK bRoWn'],
['FrançAIS is ÜBER-åwesome', 'françAIS is ÜBER-åwesome'],
['Über', 'über'],
['Åwesome', 'åwesome'],
// Add a multibyte string.
['ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ', 'αΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ', TRUE],
];
}
/**
* Tests multibyte ucwords.
*
* @dataProvider providerUcwords
* @covers ::ucwords
*/
public function testUcwords($text, $expected, $multibyte = FALSE) {
$status = $multibyte ? Unicode::STATUS_MULTIBYTE : Unicode::STATUS_SINGLEBYTE;
Unicode::setStatus($status);
$this->assertEquals($expected, Unicode::ucwords($text));
}
/**
* Data provider for testUcwords().
*
* @see testUcwords()
*
* @return array
* An array containing a string, its capitalized version and whether it should
* be processed as multibyte.
*/
public function providerUcwords() {
return [
['tHe QUIcK bRoWn', 'THe QUIcK BRoWn'],
['françAIS', 'FrançAIS'],
['über', 'Über'],
['åwesome', 'Åwesome'],
// Make sure we don't mangle extra spaces.
['frànçAIS is über-åwesome', 'FrànçAIS Is Über-Åwesome'],
// Add a multibyte string.
['σion', 'Σion', TRUE],
];
}
/**
* Tests multibyte strlen.
*
* @dataProvider providerStrlen
* @covers ::strlen
*/
public function testStrlen($text, $expected) {
// Run through multibyte code path.
Unicode::setStatus(Unicode::STATUS_MULTIBYTE);
$this->assertEquals($expected, Unicode::strlen($text));
// Run through singlebyte code path.
Unicode::setStatus(Unicode::STATUS_SINGLEBYTE);
$this->assertEquals($expected, Unicode::strlen($text));
}
/**
* Data provider for testStrlen().
*
* @see testStrlen()
*
* @return array
* An array containing a string and its length.
*/
public function providerStrlen() {
return [
['tHe QUIcK bRoWn', 15],
['ÜBER-åwesome', 12],
['以呂波耳・ほへとち。リヌルヲ。', 15],
];
}
/**
* Tests multibyte substr.
*
* @dataProvider providerSubstr
* @covers ::substr
*/
public function testSubstr($text, $start, $length, $expected) {
// Run through multibyte code path.
Unicode::setStatus(Unicode::STATUS_MULTIBYTE);
$this->assertEquals($expected, Unicode::substr($text, $start, $length));
// Run through singlebyte code path.
Unicode::setStatus(Unicode::STATUS_SINGLEBYTE);
$this->assertEquals($expected, Unicode::substr($text, $start, $length));
}
/**
* Data provider for testSubstr().
*
* @see testSubstr()
*
* @return array
* An array containing:
* - The string to test.
* - The start number to be processed by substr.
* - The length number to be processed by substr.
* - The expected string result.
*/
public function providerSubstr() {
return [
['frànçAIS is über-åwesome', 0, NULL, 'frànçAIS is über-åwesome'],
['frànçAIS is über-åwesome', 0, 0, ''],
['frànçAIS is über-åwesome', 0, 1, 'f'],
['frànçAIS is über-åwesome', 0, 8, 'frànçAIS'],
['frànçAIS is über-åwesome', 0, 23, 'frànçAIS is über-åwesom'],
['frànçAIS is über-åwesome', 0, 24, 'frànçAIS is über-åwesome'],
['frànçAIS is über-åwesome', 0, 25, 'frànçAIS is über-åwesome'],
['frànçAIS is über-åwesome', 0, 100, 'frànçAIS is über-åwesome'],
['frànçAIS is über-åwesome', 4, 4, 'çAIS'],
['frànçAIS is über-åwesome', 1, 0, ''],
['frànçAIS is über-åwesome', 100, 0, ''],
['frànçAIS is über-åwesome', -4, 2, 'so'],
['frànçAIS is über-åwesome', -4, 3, 'som'],
['frànçAIS is über-åwesome', -4, 4, 'some'],
['frànçAIS is über-åwesome', -4, 5, 'some'],
['frànçAIS is über-åwesome', -7, 10, 'åwesome'],
['frànçAIS is über-åwesome', 5, -10, 'AIS is üb'],
['frànçAIS is über-åwesome', 0, -10, 'frànçAIS is üb'],
['frànçAIS is über-åwesome', 0, -1, 'frànçAIS is über-åwesom'],
['frànçAIS is über-åwesome', -7, -2, 'åweso'],
['frànçAIS is über-åwesome', -7, -6, 'å'],
['frànçAIS is über-åwesome', -7, -7, ''],
['frànçAIS is über-åwesome', -7, -8, ''],
['...', 0, 2, '..'],
['以呂波耳・ほへとち。リヌルヲ。', 1, 3, '呂波耳'],
];
}
/**
* Tests multibyte truncate.
*
* @dataProvider providerTruncate
* @covers ::truncate
*/
public function testTruncate($text, $max_length, $expected, $wordsafe = FALSE, $add_ellipsis = FALSE) {
$this->assertEquals($expected, Unicode::truncate($text, $max_length, $wordsafe, $add_ellipsis));
}
/**
* Data provider for testTruncate().
*
* @see testTruncate()
*
* @return array
* An array containing:
* - The string to test.
* - The max length to truncate this string to.
* - The expected string result.
* - (optional) Boolean for the $wordsafe flag. Defaults to FALSE.
* - (optional) Boolean for the $add_ellipsis flag. Defaults to FALSE.
*/
public function providerTruncate() {
return [
['frànçAIS is über-åwesome', 24, 'frànçAIS is über-åwesome'],
['frànçAIS is über-åwesome', 23, 'frànçAIS is über-åwesom'],
['frànçAIS is über-åwesome', 17, 'frànçAIS is über-'],
['以呂波耳・ほへとち。リヌルヲ。', 6, '以呂波耳・ほ'],
['frànçAIS is über-åwesome', 24, 'frànçAIS is über-åwesome', FALSE, TRUE],
['frànçAIS is über-åwesome', 23, 'frànçAIS is über-åweso…', FALSE, TRUE],
['frànçAIS is über-åwesome', 17, 'frànçAIS is über…', FALSE, TRUE],
['123', 1, '…', TRUE, TRUE],
['123', 2, '1…', TRUE, TRUE],
['123', 3, '123', TRUE, TRUE],
['1234', 3, '12…', TRUE, TRUE],
['1234567890', 10, '1234567890', TRUE, TRUE],
['12345678901', 10, '123456789…', TRUE, TRUE],
['12345678901', 11, '12345678901', TRUE, TRUE],
['123456789012', 11, '1234567890…', TRUE, TRUE],
['12345 7890', 10, '12345 7890', TRUE, TRUE],
['12345 7890', 9, '12345…', TRUE, TRUE],
['123 567 90', 10, '123 567 90', TRUE, TRUE],
['123 567 901', 10, '123 567…', TRUE, TRUE],
['Stop. Hammertime.', 17, 'Stop. Hammertime.', TRUE, TRUE],
['Stop. Hammertime.', 16, 'Stop…', TRUE, TRUE],
['frànçAIS is über-åwesome', 24, 'frànçAIS is über-åwesome', TRUE, TRUE],
['frànçAIS is über-åwesome', 23, 'frànçAIS is über…', TRUE, TRUE],
['frànçAIS is über-åwesome', 17, 'frànçAIS is über…', TRUE, TRUE],
['¿Dónde está el niño?', 20, '¿Dónde está el niño?', TRUE, TRUE],
['¿Dónde está el niño?', 19, '¿Dónde está el…', TRUE, TRUE],
['¿Dónde está el niño?', 13, '¿Dónde está…', TRUE, TRUE],
['¿Dónde está el niño?', 10, '¿Dónde…', TRUE, TRUE],
['Help! Help! Help!', 17, 'Help! Help! Help!', TRUE, TRUE],
['Help! Help! Help!', 16, 'Help! Help!…', TRUE, TRUE],
['Help! Help! Help!', 15, 'Help! Help!…', TRUE, TRUE],
['Help! Help! Help!', 14, 'Help! Help!…', TRUE, TRUE],
['Help! Help! Help!', 13, 'Help! Help!…', TRUE, TRUE],
['Help! Help! Help!', 12, 'Help! Help!…', TRUE, TRUE],
['Help! Help! Help!', 11, 'Help! Help…', TRUE, TRUE],
['Help! Help! Help!', 10, 'Help!…', TRUE, TRUE],
['Help! Help! Help!', 9, 'Help!…', TRUE, TRUE],
['Help! Help! Help!', 8, 'Help!…', TRUE, TRUE],
['Help! Help! Help!', 7, 'Help!…', TRUE, TRUE],
['Help! Help! Help!', 6, 'Help!…', TRUE, TRUE],
['Help! Help! Help!', 5, 'Help…', TRUE, TRUE],
['Help! Help! Help!', 4, 'Hel…', TRUE, TRUE],
['Help! Help! Help!', 3, 'He…', TRUE, TRUE],
['Help! Help! Help!', 2, 'H…', TRUE, TRUE],
];
}
/**
* Tests multibyte truncate bytes.
*
* @dataProvider providerTestTruncateBytes
* @covers ::truncateBytes
*
* @param string $text
* The string to truncate.
* @param int $max_length
* The upper limit on the returned string length.
* @param string $expected
* The expected return from Unicode::truncateBytes().
*/
public function testTruncateBytes($text, $max_length, $expected) {
$this->assertEquals($expected, Unicode::truncateBytes($text, $max_length), 'The string was not correctly truncated.');
}
/**
* Provides data for self::testTruncateBytes().
*
* @return array
* An array of arrays, each containing the parameters to
* self::testTruncateBytes().
*/
public function providerTestTruncateBytes() {
return [
// String shorter than max length.
['Short string', 42, 'Short string'],
// Simple string longer than max length.
['Longer string than previous.', 10, 'Longer str'],
// Unicode.
['以呂波耳・ほへとち。リヌルヲ。', 10, '以呂波'],
];
}
/**
* Tests UTF-8 validation.
*
* @dataProvider providerTestValidateUtf8
* @covers ::validateUtf8
*
* @param string $text
* The text to validate.
* @param bool $expected
* The expected return value from Unicode::validateUtf8().
* @param string $message
* The message to display on failure.
*/
public function testValidateUtf8($text, $expected, $message) {
$this->assertEquals($expected, Unicode::validateUtf8($text), $message);
}
/**
* Provides data for self::testValidateUtf8().
*
* Invalid UTF-8 examples sourced from http://stackoverflow.com/a/11709412/109119.
*
* @return array
* An array of arrays, each containing the parameters for
* self::testValidateUtf8().
*/
public function providerTestValidateUtf8() {
return [
// Empty string.
['', TRUE, 'An empty string did not validate.'],
// Simple text string.
['Simple text.', TRUE, 'A simple ASCII text string did not validate.'],
// Invalid UTF-8, overlong 5 byte encoding.
[chr(0xF8) . chr(0x80) . chr(0x80) . chr(0x80) . chr(0x80), FALSE, 'Invalid UTF-8 was validated.'],
// High code-point without trailing characters.
[chr(0xD0) . chr(0x01), FALSE, 'Invalid UTF-8 was validated.'],
];
}
/**
* Tests UTF-8 conversion.
*
* @dataProvider providerTestConvertToUtf8
* @covers ::convertToUtf8
*
* @param string $data
* The data to be converted.
* @param string $encoding
* The encoding the data is in.
* @param string|bool $expected
* The expected result.
*/
public function testConvertToUtf8($data, $encoding, $expected) {
$this->assertEquals($expected, Unicode::convertToUtf8($data, $encoding));
}
/**
* Provides data to self::testConvertToUtf8().
*
* @return array
* An array of arrays, each containing the parameters to
* self::testConvertUtf8(). }
*/
public function providerTestConvertToUtf8() {
return [
[chr(0x97), 'Windows-1252', '—'],
[chr(0x99), 'Windows-1252', '™'],
[chr(0x80), 'Windows-1252', '€'],
];
}
/**
* Tests multibyte strpos.
*
* @dataProvider providerStrpos
* @covers ::strpos
*/
public function testStrpos($haystack, $needle, $offset, $expected) {
// Run through multibyte code path.
Unicode::setStatus(Unicode::STATUS_MULTIBYTE);
$this->assertEquals($expected, Unicode::strpos($haystack, $needle, $offset));
// Run through singlebyte code path.
Unicode::setStatus(Unicode::STATUS_SINGLEBYTE);
$this->assertEquals($expected, Unicode::strpos($haystack, $needle, $offset));
}
/**
* Data provider for testStrpos().
*
* @see testStrpos()
*
* @return array
* An array containing:
* - The haystack string to be searched in.
* - The needle string to search for.
* - The offset integer to start at.
* - The expected integer/FALSE result.
*/
public function providerStrpos() {
return [
['frànçAIS is über-åwesome', 'frànçAIS is über-åwesome', 0, 0],
['frànçAIS is über-åwesome', 'rànçAIS is über-åwesome', 0, 1],
['frànçAIS is über-åwesome', 'not in string', 0, FALSE],
['frànçAIS is über-åwesome', 'r', 0, 1],
['frànçAIS is über-åwesome', 'nçAIS', 0, 3],
['frànçAIS is über-åwesome', 'nçAIS', 2, 3],
['frànçAIS is über-åwesome', 'nçAIS', 3, 3],
['以呂波耳・ほへとち。リヌルヲ。', '波耳', 0, 2],
['以呂波耳・ほへとち。リヌルヲ。', '波耳', 1, 2],
['以呂波耳・ほへとち。リヌルヲ。', '波耳', 2, 2],
];
}
}
@@ -0,0 +1,588 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Tests\UnitTestCase;
/**
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\UrlHelper
*/
class UrlHelperTest extends UnitTestCase {
/**
* Provides test data for testBuildQuery().
*
* @return array
*/
public function providerTestBuildQuery() {
return [
[['a' => ' &#//+%20@۞'], 'a=%20%26%23//%2B%2520%40%DB%9E', 'Value was properly encoded.'],
[[' &#//+%20@۞' => 'a'], '%20%26%23%2F%2F%2B%2520%40%DB%9E=a', 'Key was properly encoded.'],
[['a' => '1', 'b' => '2', 'c' => '3'], 'a=1&b=2&c=3', 'Multiple values were properly concatenated.'],
[['a' => ['b' => '2', 'c' => '3'], 'd' => 'foo'], 'a[b]=2&a[c]=3&d=foo', 'Nested array was properly encoded.'],
[['foo' => NULL], 'foo', 'Simple parameters are properly added.'],
];
}
/**
* Tests query building.
*
* @dataProvider providerTestBuildQuery
* @covers ::buildQuery
*
* @param array $query
* The array of query parameters.
* @param string $expected
* The expected query string.
* @param string $message
* The assertion message.
*/
public function testBuildQuery($query, $expected, $message) {
$this->assertEquals(UrlHelper::buildQuery($query), $expected, $message);
}
/**
* Data provider for testValidAbsolute().
*
* @return array
*/
public function providerTestValidAbsoluteData() {
$urls = [
'example.com',
'www.example.com',
'ex-ample.com',
'3xampl3.com',
'example.com/parenthesis',
'example.com/index.html#pagetop',
'example.com:8080',
'subdomain.example.com',
'example.com/index.php/node',
'example.com/index.php/node?param=false',
'user@www.example.com',
'user:pass@www.example.com:8080/login.php?do=login&style=%23#pagetop',
'127.0.0.1',
'example.org?',
'john%20doe:secret:foo@example.org/',
'example.org/~,$\'*;',
'caf%C3%A9.example.org',
'[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html',
];
return $this->dataEnhanceWithScheme($urls);
}
/**
* Tests valid absolute URLs.
*
* @dataProvider providerTestValidAbsoluteData
* @covers ::isValid
*
* @param string $url
* The url to test.
* @param string $scheme
* The scheme to test.
*/
public function testValidAbsolute($url, $scheme) {
$test_url = $scheme . '://' . $url;
$valid_url = UrlHelper::isValid($test_url, TRUE);
$this->assertTrue($valid_url, $test_url . ' is a valid URL.');
}
/**
* Provides data for testInvalidAbsolute().
*
* @return array
*/
public function providerTestInvalidAbsolute() {
$data = [
'',
'ex!ample.com',
'ex%ample.com',
];
return $this->dataEnhanceWithScheme($data);
}
/**
* Tests invalid absolute URLs.
*
* @dataProvider providerTestInvalidAbsolute
* @covers ::isValid
*
* @param string $url
* The url to test.
* @param string $scheme
* The scheme to test.
*/
public function testInvalidAbsolute($url, $scheme) {
$test_url = $scheme . '://' . $url;
$valid_url = UrlHelper::isValid($test_url, TRUE);
$this->assertFalse($valid_url, $test_url . ' is NOT a valid URL.');
}
/**
* Provides data for testValidRelative().
*
* @return array
*/
public function providerTestValidRelativeData() {
$data = [
'paren(the)sis',
'index.html#pagetop',
'index.php/node',
'index.php/node?param=false',
'login.php?do=login&style=%23#pagetop',
];
return $this->dataEnhanceWithPrefix($data);
}
/**
* Tests valid relative URLs.
*
* @dataProvider providerTestValidRelativeData
* @covers ::isValid
*
* @param string $url
* The url to test.
* @param string $prefix
* The prefix to test.
*/
public function testValidRelative($url, $prefix) {
$test_url = $prefix . $url;
$valid_url = UrlHelper::isValid($test_url);
$this->assertTrue($valid_url, $test_url . ' is a valid URL.');
}
/**
* Provides data for testInvalidRelative().
*
* @return array
*/
public function providerTestInvalidRelativeData() {
$data = [
'ex^mple',
'example<>',
'ex%ample',
];
return $this->dataEnhanceWithPrefix($data);
}
/**
* Tests invalid relative URLs.
*
* @dataProvider providerTestInvalidRelativeData
* @covers ::isValid
*
* @param string $url
* The url to test.
* @param string $prefix
* The prefix to test.
*/
public function testInvalidRelative($url, $prefix) {
$test_url = $prefix . $url;
$valid_url = UrlHelper::isValid($test_url);
$this->assertFalse($valid_url, $test_url . ' is NOT a valid URL.');
}
/**
* Tests query filtering.
*
* @dataProvider providerTestFilterQueryParameters
* @covers ::filterQueryParameters
*
* @param array $query
* The array of query parameters.
* @param array $exclude
* A list of $query array keys to remove. Use "parent[child]" to exclude
* nested items.
* @param array $expected
* An array containing query parameters.
*/
public function testFilterQueryParameters($query, $exclude, $expected) {
$filtered = UrlHelper::filterQueryParameters($query, $exclude);
$this->assertEquals($expected, $filtered, 'The query was not properly filtered.');
}
/**
* Provides data to self::testFilterQueryParameters().
*
* @return array
*/
public static function providerTestFilterQueryParameters() {
return [
// Test without an exclude filter.
[
'query' => ['a' => ['b' => 'c']],
'exclude' => [],
'expected' => ['a' => ['b' => 'c']],
],
// Exclude the 'b' element.
[
'query' => ['a' => ['b' => 'c', 'd' => 'e']],
'exclude' => ['a[b]'],
'expected' => ['a' => ['d' => 'e']],
],
];
}
/**
* Tests url parsing.
*
* @dataProvider providerTestParse
* @covers ::parse
*
* @param string $url
* URL to test.
* @param array $expected
* Associative array with expected parameters.
*/
public function testParse($url, $expected) {
$parsed = UrlHelper::parse($url);
$this->assertEquals($expected, $parsed, 'The URL was not properly parsed.');
}
/**
* Provides data for self::testParse().
*
* @return array
*/
public static function providerTestParse() {
return [
[
'http://www.example.com/my/path',
[
'path' => 'http://www.example.com/my/path',
'query' => [],
'fragment' => '',
],
],
[
'http://www.example.com/my/path?destination=home#footer',
[
'path' => 'http://www.example.com/my/path',
'query' => [
'destination' => 'home',
],
'fragment' => 'footer',
],
],
[
'http://',
[
'path' => '',
'query' => [],
'fragment' => '',
],
],
[
'https://',
[
'path' => '',
'query' => [],
'fragment' => '',
],
],
[
'/my/path?destination=home#footer',
[
'path' => '/my/path',
'query' => [
'destination' => 'home',
],
'fragment' => 'footer',
],
],
];
}
/**
* Tests path encoding.
*
* @dataProvider providerTestEncodePath
* @covers ::encodePath
*
* @param string $path
* A path to encode.
* @param string $expected
* The expected encoded path.
*/
public function testEncodePath($path, $expected) {
$encoded = UrlHelper::encodePath($path);
$this->assertEquals($expected, $encoded);
}
/**
* Provides data for self::testEncodePath().
*
* @return array
*/
public static function providerTestEncodePath() {
return [
['unencoded path with spaces', 'unencoded%20path%20with%20spaces'],
['slashes/should/be/preserved', 'slashes/should/be/preserved'],
];
}
/**
* Tests external versus internal paths.
*
* @dataProvider providerTestIsExternal
* @covers ::isExternal
*
* @param string $path
* URL or path to test.
* @param bool $expected
* Expected result.
*/
public function testIsExternal($path, $expected) {
$isExternal = UrlHelper::isExternal($path);
$this->assertEquals($expected, $isExternal);
}
/**
* Provides data for self::testIsExternal().
*
* @return array
*/
public static function providerTestIsExternal() {
return [
['/internal/path', FALSE],
['https://example.com/external/path', TRUE],
['javascript://fake-external-path', FALSE],
// External URL without an explicit protocol.
['//www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE],
// Internal URL starting with a slash.
['/www.drupal.org', FALSE],
// Simple external URLs.
['http://example.com', TRUE],
['https://example.com', TRUE],
['http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE],
['//drupal.org', TRUE],
// Some browsers ignore or strip leading control characters.
["\x00//www.example.com", TRUE],
["\x08//www.example.com", TRUE],
["\x1F//www.example.com", TRUE],
["\n//www.example.com", TRUE],
// JSON supports decoding directly from UTF-8 code points.
[json_decode('"\u00AD"') . "//www.example.com", TRUE],
[json_decode('"\u200E"') . "//www.example.com", TRUE],
[json_decode('"\uE0020"') . "//www.example.com", TRUE],
[json_decode('"\uE000"') . "//www.example.com", TRUE],
// Backslashes should be normalized to forward.
['\\\\example.com', TRUE],
// Local URLs.
['node', FALSE],
['/system/ajax', FALSE],
['?q=foo:bar', FALSE],
['node/edit:me', FALSE],
['/drupal.org', FALSE],
['<front>', FALSE],
];
}
/**
* Tests bad protocol filtering and escaping.
*
* @dataProvider providerTestFilterBadProtocol
* @covers ::setAllowedProtocols
* @covers ::filterBadProtocol
*
* @param string $uri
* Protocol URI.
* @param string $expected
* Expected escaped value.
* @param array $protocols
* Protocols to allow.
*/
public function testFilterBadProtocol($uri, $expected, $protocols) {
UrlHelper::setAllowedProtocols($protocols);
$this->assertEquals($expected, UrlHelper::filterBadProtocol($uri));
// Multiple calls to UrlHelper::filterBadProtocol() do not cause double
// escaping.
$this->assertEquals($expected, UrlHelper::filterBadProtocol(UrlHelper::filterBadProtocol($uri)));
}
/**
* Provides data for self::testTestFilterBadProtocol().
*
* @return array
*/
public static function providerTestFilterBadProtocol() {
return [
['javascript://example.com?foo&bar', '//example.com?foo&amp;bar', ['http', 'https']],
// Test custom protocols.
['http://example.com?foo&bar', '//example.com?foo&amp;bar', ['https']],
// Valid protocol.
['http://example.com?foo&bar', 'http://example.com?foo&amp;bar', ['https', 'http']],
// Colon not part of the URL scheme.
['/test:8888?foo&bar', '/test:8888?foo&amp;bar', ['http']],
];
}
/**
* Tests dangerous url protocol filtering.
*
* @dataProvider providerTestStripDangerousProtocols
* @covers ::setAllowedProtocols
* @covers ::stripDangerousProtocols
*
* @param string $uri
* Protocol URI.
* @param string $expected
* Expected escaped value.
* @param array $protocols
* Protocols to allow.
*/
public function testStripDangerousProtocols($uri, $expected, $protocols) {
UrlHelper::setAllowedProtocols($protocols);
$stripped = UrlHelper::stripDangerousProtocols($uri);
$this->assertEquals($expected, $stripped);
}
/**
* Provides data for self::testStripDangerousProtocols().
*
* @return array
*/
public static function providerTestStripDangerousProtocols() {
return [
['javascript://example.com', '//example.com', ['http', 'https']],
// Test custom protocols.
['http://example.com', '//example.com', ['https']],
// Valid protocol.
['http://example.com', 'http://example.com', ['https', 'http']],
// Colon not part of the URL scheme.
['/test:8888', '/test:8888', ['http']],
];
}
/**
* Enhances test urls with schemes
*
* @param array $urls
* The list of urls.
*
* @return array
* A list of provider data with schemes.
*/
protected function dataEnhanceWithScheme(array $urls) {
$url_schemes = ['http', 'https', 'ftp'];
$data = [];
foreach ($url_schemes as $scheme) {
foreach ($urls as $url) {
$data[] = [$url, $scheme];
}
}
return $data;
}
/**
* Enhances test urls with prefixes.
*
* @param array $urls
* The list of urls.
*
* @return array
* A list of provider data with prefixes.
*/
protected function dataEnhanceWithPrefix(array $urls) {
$prefixes = ['', '/'];
$data = [];
foreach ($prefixes as $prefix) {
foreach ($urls as $url) {
$data[] = [$url, $prefix];
}
}
return $data;
}
/**
* Test detecting external urls that point to local resources.
*
* @param string $url
* The external url to test.
* @param string $base_url
* The base url.
* @param bool $expected
* TRUE if an external URL points to this installation as determined by the
* base url.
*
* @covers ::externalIsLocal
* @dataProvider providerTestExternalIsLocal
*/
public function testExternalIsLocal($url, $base_url, $expected) {
$this->assertSame($expected, UrlHelper::externalIsLocal($url, $base_url));
}
/**
* Provider for local external url detection.
*
* @see \Drupal\Tests\Component\Utility\UrlHelperTest::testExternalIsLocal()
*/
public function providerTestExternalIsLocal() {
return [
// Different mixes of trailing slash.
['http://example.com', 'http://example.com', TRUE],
['http://example.com/', 'http://example.com', TRUE],
['http://example.com', 'http://example.com/', TRUE],
['http://example.com/', 'http://example.com/', TRUE],
// Sub directory of site.
['http://example.com/foo', 'http://example.com/', TRUE],
['http://example.com/foo/bar', 'http://example.com/foo', TRUE],
['http://example.com/foo/bar', 'http://example.com/foo/', TRUE],
// Different sub-domain.
['http://example.com', 'http://www.example.com/', FALSE],
['http://example.com/', 'http://www.example.com/', FALSE],
['http://example.com/foo', 'http://www.example.com/', FALSE],
// Different TLD.
['http://example.com', 'http://example.ca', FALSE],
['http://example.com', 'http://example.ca/', FALSE],
['http://example.com/', 'http://example.ca/', FALSE],
['http://example.com/foo', 'http://example.ca', FALSE],
['http://example.com/foo', 'http://example.ca/', FALSE],
// Different site path.
['http://example.com/foo', 'http://example.com/bar', FALSE],
['http://example.com', 'http://example.com/bar', FALSE],
['http://example.com/bar', 'http://example.com/bar/', FALSE],
];
}
/**
* Test invalid url arguments.
*
* @param string $url
* The url to test.
* @param string $base_url
* The base url.
*
* @covers ::externalIsLocal
* @dataProvider providerTestExternalIsLocalInvalid
*/
public function testExternalIsLocalInvalid($url, $base_url) {
$this->setExpectedException(\InvalidArgumentException::class);
UrlHelper::externalIsLocal($url, $base_url);
}
/**
* Provides invalid argument data for local external url detection.
*
* @see \Drupal\Tests\Component\Utility\UrlHelperTest::testExternalIsLocalInvalid()
*/
public function providerTestExternalIsLocalInvalid() {
return [
['http://example.com/foo', ''],
['http://example.com/foo', 'bar'],
['http://example.com/foo', 'http://'],
// Invalid destination urls.
['', 'http://example.com/foo'],
['bar', 'http://example.com/foo'],
['/bar', 'http://example.com/foo'],
['bar/', 'http://example.com/foo'],
['http://', 'http://example.com/foo'],
];
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\UserAgent;
use Drupal\Tests\UnitTestCase;
/**
* Tests bytes size parsing helper methods.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\UserAgent
*/
class UserAgentTest extends UnitTestCase {
/**
* Helper method to supply language codes to testGetBestMatchingLangcode().
*
* @return array
* Language codes, ordered by priority.
*/
protected function getLanguages() {
return [
// In our test case, 'en' has priority over 'en-US'.
'en',
'en-US',
// But 'fr-CA' has priority over 'fr'.
'fr-CA',
'fr',
// 'es-MX' is alone.
'es-MX',
// 'pt' is alone.
'pt',
// Language codes with more then one dash are actually valid.
// eh-oh-laa-laa is the official language code of the Teletubbies.
'eh-oh-laa-laa',
// Chinese languages.
'zh-hans',
'zh-hant',
'zh-hant-tw',
];
}
/**
* Helper method to supply language mappings to testGetBestMatchingLangcode().
*
* @return array
* Language mappings.
*/
protected function getMappings() {
return [
'no' => 'nb',
'pt' => 'pt-pt',
'zh' => 'zh-hans',
'zh-tw' => 'zh-hant',
'zh-hk' => 'zh-hant',
'zh-mo' => 'zh-hant',
'zh-cht' => 'zh-hant',
'zh-cn' => 'zh-hans',
'zh-sg' => 'zh-hans',
'zh-chs' => 'zh-hans',
];
}
/**
* Test matching language from user agent.
*
* @dataProvider providerTestGetBestMatchingLangcode
* @covers ::getBestMatchingLangcode
*/
public function testGetBestMatchingLangcode($accept_language, $expected) {
$result = UserAgent::getBestMatchingLangcode($accept_language, $this->getLanguages(), $this->getMappings());
$this->assertSame($expected, $result);
}
/**
* Data provider for testGetBestMatchingLangcode().
*
* @return array
* - An accept-language string.
* - Expected best matching language code.
*/
public function providerTestGetBestMatchingLangcode() {
return [
// Equal qvalue for each language, choose the site preferred one.
['en,en-US,fr-CA,fr,es-MX', 'en'],
['en-US,en,fr-CA,fr,es-MX', 'en'],
['fr,en', 'en'],
['en,fr', 'en'],
['en-US,fr', 'en-US'],
['fr,en-US', 'en-US'],
['fr,fr-CA', 'fr-CA'],
['fr-CA,fr', 'fr-CA'],
['fr', 'fr-CA'],
['fr;q=1', 'fr-CA'],
['fr,es-MX', 'fr-CA'],
['fr,es', 'fr-CA'],
['es,fr', 'fr-CA'],
['es-MX,de', 'es-MX'],
['de,es-MX', 'es-MX'],
// Different cases and whitespace.
['en', 'en'],
['En', 'en'],
['EN', 'en'],
[' en', 'en'],
['en ', 'en'],
['en, fr', 'en'],
// A less specific language from the browser matches a more specific one
// from the website, and the other way around for compatibility with
// some versions of Internet Explorer.
['es', 'es-MX'],
['es-MX', 'es-MX'],
['pt', 'pt'],
['pt-PT', 'pt'],
['pt-PT;q=0.5,pt-BR;q=1,en;q=0.7', 'en'],
['pt-PT;q=1,pt-BR;q=0.5,en;q=0.7', 'en'],
['pt-PT;q=0.4,pt-BR;q=0.1,en;q=0.7', 'en'],
['pt-PT;q=0.1,pt-BR;q=0.4,en;q=0.7', 'en'],
// Language code with several dashes are valid. The less specific language
// from the browser matches the more specific one from the website.
['eh-oh-laa-laa', 'eh-oh-laa-laa'],
['eh-oh-laa', 'eh-oh-laa-laa'],
['eh-oh', 'eh-oh-laa-laa'],
['eh', 'eh-oh-laa-laa'],
// Different qvalues.
['fr,en;q=0.5', 'fr-CA'],
['fr,en;q=0.5,fr-CA;q=0.25', 'fr'],
// Silly wildcards are also valid.
['*,fr-CA;q=0.5', 'en'],
['*,en;q=0.25', 'fr-CA'],
['en,en-US;q=0.5,fr;q=0.25', 'en'],
['en-US,en;q=0.5,fr;q=0.25', 'en-US'],
// Unresolvable cases.
['', FALSE],
['de,pl', FALSE],
['iecRswK4eh', FALSE],
[$this->randomMachineName(10), FALSE],
// Chinese langcodes.
['zh-cn, en-us;q=0.90, en;q=0.80, zh;q=0.70', 'zh-hans'],
['zh-tw, en-us;q=0.90, en;q=0.80, zh;q=0.70', 'zh-hant'],
['zh-hant, en-us;q=0.90, en;q=0.80, zh;q=0.70', 'zh-hant'],
['zh-hans, en-us;q=0.90, en;q=0.80, zh;q=0.70', 'zh-hans'],
// @todo: This is copied from RFC4647 but our regex skips the numbers so
// they where removed. Our code should be updated so private1-private2 is
// valid. http://tools.ietf.org/html/rfc4647#section-3.4
['zh-hant-CN-x-private-private, en-us;q=0.90, en;q=0.80, zh;q=0.70', 'zh-hant'],
['zh-cn', 'zh-hans'],
['zh-sg', 'zh-hans'],
['zh-tw', 'zh-hant'],
['zh-hk', 'zh-hant'],
['zh-mo', 'zh-hant'],
['zh-hans', 'zh-hans'],
['zh-hant', 'zh-hant'],
['zh-chs', 'zh-hans'],
['zh-cht', 'zh-hant'],
];
}
}
@@ -0,0 +1,125 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Utility\VariableTest.
*/
namespace Drupal\Tests\Component\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\Variable;
/**
* Test variable export functionality in Variable component.
*
* @group Variable
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Variable
*/
class VariableTest extends UnitTestCase {
/**
* Data provider for testExport().
*
* @return array
* An array containing:
* - The expected export string.
* - The variable to export.
*/
public function providerTestExport() {
return [
// Array.
[
'array()',
[],
],
[
// non-associative.
"array(\n 1,\n 2,\n 3,\n 4,\n)",
[1, 2, 3, 4],
],
[
// associative.
"array(\n 'a' => 1,\n)",
['a' => 1],
],
// Bool.
[
'TRUE',
TRUE,
],
[
'FALSE',
FALSE,
],
// Strings.
[
"'string'",
'string',
],
[
'"\n\r\t"',
"\n\r\t",
],
[
// 2 backslashes. \\
"'\\'",
'\\',
],
[
// Double-quote "
"'\"'",
"\"",
],
[
// Single-quote '
'"\'"',
"'",
],
[
// Quotes with $ symbols.
'"\$settings[\'foo\']"',
'$settings[\'foo\']',
],
// Object.
[
// A stdClass object.
'(object) array()',
new \stdClass(),
],
[
// A not-stdClass object.
"Drupal\Tests\Component\Utility\StubVariableTestClass::__set_state(array(\n))",
new StubVariableTestClass(),
],
];
}
/**
* Tests exporting variables.
*
* @dataProvider providerTestExport
* @covers ::export
*
* @param string $expected
* The expected exported variable.
* @param mixed $variable
* The variable to be exported.
*/
public function testExport($expected, $variable) {
$this->assertEquals($expected, Variable::export($variable));
}
}
/**
* No-op test class for VariableTest::testExport().
*
* @see Drupal\Tests\Component\Utility\VariableTest::testExport()
* @see Drupal\Tests\Component\Utility\VariableTest::providerTestExport()
*/
class StubVariableTestClass {
}
@@ -0,0 +1,622 @@
<?php
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Tests\UnitTestCase;
/**
* XSS Filtering tests.
*
* @group Utility
*
* @coversDefaultClass \Drupal\Component\Utility\Xss
*
* Script injection vectors mostly adopted from http://ha.ckers.org/xss.html.
*
* Relevant CVEs:
* - CVE-2002-1806, ~CVE-2005-0682, ~CVE-2005-2106, CVE-2005-3973,
* CVE-2006-1226 (= rev. 1.112?), CVE-2008-0273, CVE-2008-3740.
*/
class XssTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$allowed_protocols = [
'http',
'https',
'ftp',
'news',
'nntp',
'telnet',
'mailto',
'irc',
'ssh',
'sftp',
'webcal',
'rtsp',
];
UrlHelper::setAllowedProtocols($allowed_protocols);
}
/**
* Tests limiting allowed tags and XSS prevention.
*
* XSS tests assume that script is disallowed by default and src is allowed
* by default, but on* and style attributes are disallowed.
*
* @param string $value
* The value to filter.
* @param string $expected
* The expected result.
* @param string $message
* The assertion message to display upon failure.
* @param array $allowed_tags
* (optional) The allowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter().
*
* @dataProvider providerTestFilterXssNormalized
*/
public function testFilterXssNormalized($value, $expected, $message, array $allowed_tags = NULL) {
if ($allowed_tags === NULL) {
$value = Xss::filter($value);
}
else {
$value = Xss::filter($value, $allowed_tags);
}
$this->assertNormalized($value, $expected, $message);
}
/**
* Data provider for testFilterXssNormalized().
*
* @see testFilterXssNormalized()
*
* @return array
* An array of arrays containing strings:
* - The value to filter.
* - The value to expect after filtering.
* - The assertion message.
* - (optional) The allowed HTML HTML tags array that should be passed to
* \Drupal\Component\Utility\Xss::filter().
*/
public function providerTestFilterXssNormalized() {
return [
[
"Who&#039;s Online",
"who's online",
'HTML filter -- html entity number',
],
[
"Who&amp;#039;s Online",
"who&#039;s online",
'HTML filter -- encoded html entity number',
],
[
"Who&amp;amp;#039; Online",
"who&amp;#039; online",
'HTML filter -- double encoded html entity number',
],
// Custom elements with dashes in the tag name.
[
"<test-element></test-element>",
"<test-element></test-element>",
'Custom element with dashes in tag name.',
['test-element'],
],
];
}
/**
* Tests limiting to allowed tags and XSS prevention.
*
* XSS tests assume that script is disallowed by default and src is allowed
* by default, but on* and style attributes are disallowed.
*
* @param string $value
* The value to filter.
* @param string $expected
* The string that is expected to be missing.
* @param string $message
* The assertion message to display upon failure.
* @param array $allowed_tags
* (optional) The allowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter().
*
* @dataProvider providerTestFilterXssNotNormalized
*/
public function testFilterXssNotNormalized($value, $expected, $message, array $allowed_tags = NULL) {
if ($allowed_tags === NULL) {
$value = Xss::filter($value);
}
else {
$value = Xss::filter($value, $allowed_tags);
}
$this->assertNotNormalized($value, $expected, $message);
}
/**
* Data provider for testFilterXssNotNormalized().
*
* @see testFilterXssNotNormalized()
*
* @return array
* An array of arrays containing the following elements:
* - The value to filter.
* - The value to expect that's missing after filtering.
* - The assertion message.
* - (optional) The allowed HTML HTML tags array that should be passed to
* \Drupal\Component\Utility\Xss::filter().
*/
public function providerTestFilterXssNotNormalized() {
$cases = [
// Tag stripping, different ways to work around removal of HTML tags.
[
'<script>alert(0)</script>',
'script',
'HTML tag stripping -- simple script without special characters.',
],
[
'<script src="http://www.example.com" />',
'script',
'HTML tag stripping -- empty script with source.',
],
[
'<ScRipt sRc=http://www.example.com/>',
'script',
'HTML tag stripping evasion -- varying case.',
],
[
"<script\nsrc\n=\nhttp://www.example.com/\n>",
'script',
'HTML tag stripping evasion -- multiline tag.',
],
[
'<script/a src=http://www.example.com/a.js></script>',
'script',
'HTML tag stripping evasion -- non whitespace character after tag name.',
],
[
'<script/src=http://www.example.com/a.js></script>',
'script',
'HTML tag stripping evasion -- no space between tag and attribute.',
],
// Null between < and tag name works at least with IE6.
[
"<\0scr\0ipt>alert(0)</script>",
'ipt',
'HTML tag stripping evasion -- breaking HTML with nulls.',
],
[
"<scrscriptipt src=http://www.example.com/a.js>",
'script',
'HTML tag stripping evasion -- filter just removing "script".',
],
[
'<<script>alert(0);//<</script>',
'script',
'HTML tag stripping evasion -- double opening brackets.',
],
[
'<script src=http://www.example.com/a.js?<b>',
'script',
'HTML tag stripping evasion -- no closing tag.',
],
// DRUPAL-SA-2008-047: This doesn't seem exploitable, but the filter should
// work consistently.
[
'<script>>',
'script',
'HTML tag stripping evasion -- double closing tag.',
],
[
'<script src=//www.example.com/.a>',
'script',
'HTML tag stripping evasion -- no scheme or ending slash.',
],
[
'<script src=http://www.example.com/.a',
'script',
'HTML tag stripping evasion -- no closing bracket.',
],
[
'<script src=http://www.example.com/ <',
'script',
'HTML tag stripping evasion -- opening instead of closing bracket.',
],
[
'<nosuchtag attribute="newScriptInjectionVector">',
'nosuchtag',
'HTML tag stripping evasion -- unknown tag.',
],
[
'<t:set attributeName="innerHTML" to="&lt;script defer&gt;alert(0)&lt;/script&gt;">',
't:set',
'HTML tag stripping evasion -- colon in the tag name (namespaces\' tricks).',
],
[
'<img """><script>alert(0)</script>',
'script',
'HTML tag stripping evasion -- a malformed image tag.',
['img'],
],
[
'<blockquote><script>alert(0)</script></blockquote>',
'script',
'HTML tag stripping evasion -- script in a blockqoute.',
['blockquote'],
],
[
"<!--[if true]><script>alert(0)</script><![endif]-->",
'script',
'HTML tag stripping evasion -- script within a comment.',
],
// Dangerous attributes removal.
[
'<p onmouseover="http://www.example.com/">',
'onmouseover',
'HTML filter attributes removal -- events, no evasion.',
['p'],
],
[
'<li style="list-style-image: url(javascript:alert(0))">',
'style',
'HTML filter attributes removal -- style, no evasion.',
['li'],
],
[
'<img onerror =alert(0)>',
'onerror',
'HTML filter attributes removal evasion -- spaces before equals sign.',
['img'],
],
[
'<img onabort!#$%&()*~+-_.,:;?@[/|\]^`=alert(0)>',
'onabort',
'HTML filter attributes removal evasion -- non alphanumeric characters before equals sign.',
['img'],
],
[
'<img oNmediAError=alert(0)>',
'onmediaerror',
'HTML filter attributes removal evasion -- varying case.',
['img'],
],
// Works at least with IE6.
[
"<img o\0nfocus\0=alert(0)>",
'focus',
'HTML filter attributes removal evasion -- breaking with nulls.',
['img'],
],
// Only whitelisted scheme names allowed in attributes.
[
'<img src="javascript:alert(0)">',
'javascript',
'HTML scheme clearing -- no evasion.',
['img'],
],
[
'<img src=javascript:alert(0)>',
'javascript',
'HTML scheme clearing evasion -- no quotes.',
['img'],
],
// A bit like CVE-2006-0070.
[
'<img src="javascript:confirm(0)">',
'javascript',
'HTML scheme clearing evasion -- no alert ;)',
['img'],
],
[
'<img src=`javascript:alert(0)`>',
'javascript',
'HTML scheme clearing evasion -- grave accents.',
['img'],
],
[
'<img dynsrc="javascript:alert(0)">',
'javascript',
'HTML scheme clearing -- rare attribute.',
['img'],
],
[
'<table background="javascript:alert(0)">',
'javascript',
'HTML scheme clearing -- another tag.',
['table'],
],
[
'<base href="javascript:alert(0);//">',
'javascript',
'HTML scheme clearing -- one more attribute and tag.',
['base'],
],
[
'<img src="jaVaSCriPt:alert(0)">',
'javascript',
'HTML scheme clearing evasion -- varying case.',
['img'],
],
[
'<img src=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#48;&#41;>',
'javascript',
'HTML scheme clearing evasion -- UTF-8 decimal encoding.',
['img'],
],
[
'<img src=&#00000106&#0000097&#00000118&#0000097&#00000115&#0000099&#00000114&#00000105&#00000112&#00000116&#0000058&#0000097&#00000108&#00000101&#00000114&#00000116&#0000040&#0000048&#0000041>',
'javascript',
'HTML scheme clearing evasion -- long UTF-8 encoding.',
['img'],
],
[
'<img src=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x30&#x29>',
'javascript',
'HTML scheme clearing evasion -- UTF-8 hex encoding.',
['img'],
],
[
"<img src=\"jav\tascript:alert(0)\">",
'script',
'HTML scheme clearing evasion -- an embedded tab.',
['img'],
],
[
'<img src="jav&#x09;ascript:alert(0)">',
'script',
'HTML scheme clearing evasion -- an encoded, embedded tab.',
['img'],
],
[
'<img src="jav&#x000000A;ascript:alert(0)">',
'script',
'HTML scheme clearing evasion -- an encoded, embedded newline.',
['img'],
],
// With &#xD; this test would fail, but the entity gets turned into
// &amp;#xD;, so it's OK.
[
'<img src="jav&#x0D;ascript:alert(0)">',
'script',
'HTML scheme clearing evasion -- an encoded, embedded carriage return.',
['img'],
],
[
"<img src=\"\n\n\nj\na\nva\ns\ncript:alert(0)\">",
'cript',
'HTML scheme clearing evasion -- broken into many lines.',
['img'],
],
[
"<img src=\"jav\0a\0\0cript:alert(0)\">",
'cript',
'HTML scheme clearing evasion -- embedded nulls.',
['img'],
],
[
'<img src="vbscript:msgbox(0)">',
'vbscript',
'HTML scheme clearing evasion -- another scheme.',
['img'],
],
[
'<img src="nosuchscheme:notice(0)">',
'nosuchscheme',
'HTML scheme clearing evasion -- unknown scheme.',
['img'],
],
// Netscape 4.x javascript entities.
[
'<br size="&{alert(0)}">',
'alert',
'Netscape 4.x javascript entities.',
['br'],
],
// DRUPAL-SA-2008-006: Invalid UTF-8, these only work as reflected XSS with
// Internet Explorer 6.
[
"<p arg=\"\xe0\">\" style=\"background-image: url(javascript:alert(0));\"\xe0<p>",
'style',
'HTML filter -- invalid UTF-8.',
['p'],
],
];
// @fixme This dataset currently fails under 5.4 because of
// https://www.drupal.org/node/1210798. Restore after its fixed.
if (version_compare(PHP_VERSION, '5.4.0', '<')) {
$cases[] = [
'<img src=" &#14; javascript:alert(0)">',
'javascript',
'HTML scheme clearing evasion -- spaces and metacharacters before scheme.',
['img'],
];
}
return $cases;
}
/**
* Checks that invalid multi-byte sequences are rejected.
*
* @param string $value
* The value to filter.
* @param string $expected
* The expected result.
* @param string $message
* The assertion message to display upon failure.
*
* @dataProvider providerTestInvalidMultiByte
*/
public function testInvalidMultiByte($value, $expected, $message) {
$this->assertEquals(Xss::filter($value), $expected, $message);
}
/**
* Data provider for testInvalidMultiByte().
*
* @see testInvalidMultiByte()
*
* @return array
* An array of arrays containing strings:
* - The value to filter.
* - The value to expect after filtering.
* - The assertion message.
*/
public function providerTestInvalidMultiByte() {
return [
["Foo\xC0barbaz", '', 'Xss::filter() accepted invalid sequence "Foo\xC0barbaz"'],
["Fooÿñ", "Fooÿñ", 'Xss::filter() rejects valid sequence Fooÿñ"'],
["\xc0aaa", '', 'HTML filter -- overlong UTF-8 sequences.'],
];
}
/**
* Checks that strings starting with a question sign are correctly processed.
*/
public function testQuestionSign() {
$value = Xss::filter('<?xml:namespace ns="urn:schemas-microsoft-com:time">');
$this->assertTrue(stripos($value, '<?xml') === FALSE, 'HTML tag stripping evasion -- starting with a question sign (processing instructions).');
}
/**
* Check that strings in HTML attributes are correctly processed.
*
* @covers ::attributes
* @dataProvider providerTestAttributes
*/
public function testAttribute($value, $expected, $message, $allowed_tags = NULL) {
$value = Xss::filter($value, $allowed_tags);
$this->assertEquals($expected, $value, $message);
}
/**
* Data provider for testFilterXssAdminNotNormalized().
*/
public function providerTestAttributes() {
return [
[
'<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">',
'<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">',
'Image tag with alt and title attribute',
['img']
],
[
'<a href="https://www.drupal.org/" rel="dc:publisher">Drupal</a>',
'<a href="https://www.drupal.org/" rel="dc:publisher">Drupal</a>',
'Link tag with rel attribute',
['a']
],
[
'<span property="dc:subject">Drupal 8: The best release ever.</span>',
'<span property="dc:subject">Drupal 8: The best release ever.</span>',
'Span tag with property attribute',
['span']
],
[
'<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">',
'<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">',
'Image tag with data attribute',
['img']
],
[
'<a data-a2a-url="foo"></a>',
'<a data-a2a-url="foo"></a>',
'Link tag with numeric data attribute',
['a']
],
];
}
/**
* Checks that \Drupal\Component\Utility\Xss::filterAdmin() correctly strips unallowed tags.
*/
public function testFilterXSSAdmin() {
$value = Xss::filterAdmin('<style /><iframe /><frame /><frameset /><meta /><link /><embed /><applet /><param /><layer />');
$this->assertEquals($value, '', 'Admin HTML filter -- should never allow some tags.');
}
/**
* Tests the loose, admin HTML filter.
*
* @param string $value
* The value to filter.
* @param string $expected
* The expected result.
* @param string $message
* The assertion message to display upon failure.
*
* @dataProvider providerTestFilterXssAdminNotNormalized
*/
public function testFilterXssAdminNotNormalized($value, $expected, $message) {
$this->assertNotNormalized(Xss::filterAdmin($value), $expected, $message);
}
/**
* Data provider for testFilterXssAdminNotNormalized().
*
* @see testFilterXssAdminNotNormalized()
*
* @return array
* An array of arrays containing strings:
* - The value to filter.
* - The value to expect after filtering.
* - The assertion message.
*/
public function providerTestFilterXssAdminNotNormalized() {
return [
// DRUPAL-SA-2008-044
['<object />', 'object', 'Admin HTML filter -- should not allow object tag.'],
['<script />', 'script', 'Admin HTML filter -- should not allow script tag.'],
];
}
/**
* Asserts that a text transformed to lowercase with HTML entities decoded does contain a given string.
*
* Otherwise fails the test with a given message, similar to all the
* SimpleTest assert* functions.
*
* Note that this does not remove nulls, new lines and other characters that
* could be used to obscure a tag or an attribute name.
*
* @param string $haystack
* Text to look in.
* @param string $needle
* Lowercase, plain text to look for.
* @param string $message
* (optional) Message to display if failed. Defaults to an empty string.
* @param string $group
* (optional) The group this message belongs to. Defaults to 'Other'.
*/
protected function assertNormalized($haystack, $needle, $message = '', $group = 'Other') {
$this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) !== FALSE, $message, $group);
}
/**
* Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string.
*
* Otherwise fails the test with a given message, similar to all the
* SimpleTest assert* functions.
*
* Note that this does not remove nulls, new lines, and other character that
* could be used to obscure a tag or an attribute name.
*
* @param string $haystack
* Text to look in.
* @param string $needle
* Lowercase, plain text to look for.
* @param string $message
* (optional) Message to display if failed. Defaults to an empty string.
* @param string $group
* (optional) The group this message belongs to. Defaults to 'Other'.
*/
protected function assertNotNormalized($haystack, $needle, $message = '', $group = 'Other') {
$this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group);
}
}
@@ -0,0 +1,97 @@
<?php
namespace Drupal\Tests\Component\Uuid;
use Drupal\Component\Uuid\Uuid;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Component\Uuid\Com;
use Drupal\Component\Uuid\Pecl;
use Drupal\Component\Uuid\Php;
use Drupal\Tests\UnitTestCase;
/**
* Tests the handling of Universally Unique Identifiers (UUIDs).
*
* @group Uuid
*/
class UuidTest extends UnitTestCase {
/**
* Tests generating valid UUIDs.
*
* @dataProvider providerUuidInstances
*/
public function testGenerateUuid(UuidInterface $instance) {
$this->assertTrue(Uuid::isValid($instance->generate()), sprintf('UUID generation for %s works.', get_class($instance)));
}
/**
* Tests that generated UUIDs are unique.
*
* @dataProvider providerUuidInstances
*/
public function testUuidIsUnique(UuidInterface $instance) {
$this->assertNotEquals($instance->generate(), $instance->generate(), sprintf('Same UUID was not generated twice with %s.', get_class($instance)));
}
/**
* Dataprovider for UUID instance tests.
*
* @return array
*/
public function providerUuidInstances() {
$instances = [];
$instances[][] = new Php();
// If valid PECL extensions exists add to list.
if (function_exists('uuid_create') && !function_exists('uuid_make')) {
$instances[][] = new Pecl();
}
// If we are on Windows add the com implementation as well.
if (function_exists('com_create_guid')) {
$instances[][] = new Com();
}
return $instances;
}
/**
* Tests UUID validation.
*
* @param string $uuid
* The uuid to check against.
* @param bool $is_valid
* Whether the uuid is valid or not.
* @param string $message
* The message to display on failure.
*
* @dataProvider providerTestValidation
*/
public function testValidation($uuid, $is_valid, $message) {
$this->assertSame($is_valid, Uuid::isValid($uuid), $message);
}
/**
* Dataprovider for UUID instance tests.
*
* @return array
* An array of arrays containing
* - The Uuid to check against.
* - (bool) Whether or not the Uuid is valid.
* - Failure message.
*/
public function providerTestValidation() {
return [
// These valid UUIDs.
['6ba7b810-9dad-11d1-80b4-00c04fd430c8', TRUE, 'Basic FQDN UUID did not validate'],
['00000000-0000-0000-0000-000000000000', TRUE, 'Minimum UUID did not validate'],
['ffffffff-ffff-ffff-ffff-ffffffffffff', TRUE, 'Maximum UUID did not validate'],
// These are invalid UUIDs.
['0ab26e6b-f074-4e44-9da-601205fa0e976', FALSE, 'Invalid format was validated'],
['0ab26e6b-f074-4e44-9daf-1205fa0e9761f', FALSE, 'Invalid length was validated'],
];
}
}
@@ -0,0 +1,174 @@
<?php
namespace Drupal\Tests;
/**
* Tests Composer integration.
*
* @group Composer
*/
class ComposerIntegrationTest extends UnitTestCase {
/**
* Gets human-readable JSON error messages.
*
* @return string[]
* Keys are JSON_ERROR_* constants.
*/
protected function getErrorMessages() {
$messages = [
0 => 'No errors found',
JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
JSON_ERROR_SYNTAX => 'Syntax error',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
];
if (version_compare(phpversion(), '5.5.0', '>=')) {
$messages[JSON_ERROR_RECURSION] = 'One or more recursive references in the value to be encoded';
$messages[JSON_ERROR_INF_OR_NAN] = 'One or more NAN or INF values in the value to be encoded';
$messages[JSON_ERROR_UNSUPPORTED_TYPE] = 'A value of a type that cannot be encoded was given';
}
return $messages;
}
/**
* Gets the paths to the folders that contain the Composer integration.
*
* @return string[]
* The paths.
*/
protected function getPaths() {
return [
$this->root,
$this->root . '/core',
$this->root . '/core/lib/Drupal/Component/Annotation',
$this->root . '/core/lib/Drupal/Component/Assertion',
$this->root . '/core/lib/Drupal/Component/Bridge',
$this->root . '/core/lib/Drupal/Component/Datetime',
$this->root . '/core/lib/Drupal/Component/DependencyInjection',
$this->root . '/core/lib/Drupal/Component/Diff',
$this->root . '/core/lib/Drupal/Component/Discovery',
$this->root . '/core/lib/Drupal/Component/EventDispatcher',
$this->root . '/core/lib/Drupal/Component/FileCache',
$this->root . '/core/lib/Drupal/Component/FileSystem',
$this->root . '/core/lib/Drupal/Component/Gettext',
$this->root . '/core/lib/Drupal/Component/Graph',
$this->root . '/core/lib/Drupal/Component/HttpFoundation',
$this->root . '/core/lib/Drupal/Component/PhpStorage',
$this->root . '/core/lib/Drupal/Component/Plugin',
$this->root . '/core/lib/Drupal/Component/ProxyBuilder',
$this->root . '/core/lib/Drupal/Component/Render',
$this->root . '/core/lib/Drupal/Component/Serialization',
$this->root . '/core/lib/Drupal/Component/Transliteration',
$this->root . '/core/lib/Drupal/Component/Utility',
$this->root . '/core/lib/Drupal/Component/Uuid',
];
}
/**
* Tests composer.json.
*/
public function testComposerJson() {
foreach ($this->getPaths() as $path) {
$json = file_get_contents($path . '/composer.json');
$result = json_decode($json);
$this->assertNotNull($result, $this->getErrorMessages()[json_last_error()]);
}
}
/**
* Tests composer.lock content-hash.
*/
public function testComposerLockHash() {
$content_hash = self::getContentHash(file_get_contents($this->root . '/composer.json'));
$lock = json_decode(file_get_contents($this->root . '/composer.lock'), TRUE);
$this->assertSame($content_hash, $lock['content-hash']);
}
/**
* Tests core's composer.json replace section.
*
* Verify that all core modules are also listed in the 'replace' section of
* core's composer.json.
*/
public function testAllModulesReplaced() {
// Assemble a path to core modules.
$module_path = $this->root . '/core/modules';
// Grab the 'replace' section of the core composer.json file.
$json = json_decode(file_get_contents($this->root . '/core/composer.json'));
$composer_replace_packages = (array) $json->replace;
// Get a list of all the files in the module path.
$folders = scandir($module_path);
// Make sure we only deal with directories that aren't . or ..
$module_names = [];
$discard = ['.', '..'];
foreach ($folders as $file_name) {
if ((!in_array($file_name, $discard)) && is_dir($module_path . '/' . $file_name)) {
$module_names[] = $file_name;
}
}
// Assert that each core module has a corresponding 'replace' in
// composer.json.
foreach ($module_names as $module_name) {
$this->assertArrayHasKey(
'drupal/' . $module_name,
$composer_replace_packages,
'Unable to find ' . $module_name . ' in replace list of composer.json'
);
}
}
// @codingStandardsIgnoreStart
/**
* The following method is copied from \Composer\Package\Locker.
*
* @see https://github.com/composer/composer
*/
/**
* Returns the md5 hash of the sorted content of the composer file.
*
* @param string $composerFileContents The contents of the composer file.
*
* @return string
*/
protected static function getContentHash($composerFileContents)
{
$content = json_decode($composerFileContents, true);
$relevantKeys = array(
'name',
'version',
'require',
'require-dev',
'conflict',
'replace',
'provide',
'minimum-stability',
'prefer-stable',
'repositories',
'extra',
);
$relevantContent = array();
foreach (array_intersect($relevantKeys, array_keys($content)) as $key) {
$relevantContent[$key] = $content[$key];
}
if (isset($content['config']['platform'])) {
$relevantContent['config']['platform'] = $content['config']['platform'];
}
ksort($relevantContent);
return md5(json_encode($relevantContent));
}
// @codingStandardsIgnoreEnd
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Config\StorageInterface;
/**
* Provides helper methods to deal with config system objects in tests.
*/
trait ConfigTestTrait {
/**
* Returns a ConfigImporter object to import test configuration.
*
* @return \Drupal\Core\Config\ConfigImporter
* The config importer object.
*/
protected function configImporter() {
if (!$this->configImporter) {
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.sync'),
$this->container->get('config.storage'),
$this->container->get('config.manager')
);
$this->configImporter = new ConfigImporter(
$storage_comparer,
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation')
);
}
// Always recalculate the changelist when called.
return $this->configImporter->reset();
}
/**
* Copies configuration objects from source storage to target storage.
*
* @param \Drupal\Core\Config\StorageInterface $source_storage
* The source config storage service.
* @param \Drupal\Core\Config\StorageInterface $target_storage
* The target config storage service.
*/
protected function copyConfig(StorageInterface $source_storage, StorageInterface $target_storage) {
$target_storage->deleteAll();
foreach ($source_storage->listAll() as $name) {
$target_storage->write($name, $source_storage->read($name));
}
}
}
@@ -0,0 +1,572 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Access\AccessManagerTest.
*/
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\AccessException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\CheckProvider;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Access\DefaultAccessCheck;
use Drupal\Tests\UnitTestCase;
use Drupal\router_test\Access\DefinedTestAccessCheck;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @coversDefaultClass \Drupal\Core\Access\AccessManager
* @group Access
*/
class AccessManagerTest extends UnitTestCase {
/**
* The dependency injection container.
*
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $container;
/**
* The collection of routes, which are tested.
*
* @var \Symfony\Component\Routing\RouteCollection
*/
protected $routeCollection;
/**
* The access manager to test.
*
* @var \Drupal\Core\Access\AccessManager
*/
protected $accessManager;
/**
* The route provider.
*
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $routeProvider;
/**
* The parameter converter.
*
* @var \Drupal\Core\ParamConverter\ParamConverterManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $paramConverter;
/**
* The mocked account.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account;
/**
* The access arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $argumentsResolverFactory;
/**
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $currentUser;
/**
* @var \Drupal\Core\Access\CheckProvider
*/
protected $checkProvider;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container = new ContainerBuilder();
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class)->reveal();
$this->container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($this->container);
$this->routeCollection = new RouteCollection();
$this->routeCollection->add('test_route_1', new Route('/test-route-1'));
$this->routeCollection->add('test_route_2', new Route('/test-route-2', [], ['_access' => 'TRUE']));
$this->routeCollection->add('test_route_3', new Route('/test-route-3', [], ['_access' => 'FALSE']));
$this->routeCollection->add('test_route_4', new Route('/test-route-4/{value}', [], ['_access' => 'TRUE']));
$this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
$map = [];
foreach ($this->routeCollection->all() as $name => $route) {
$map[] = [$name, [], $route];
}
$map[] = ['test_route_4', ['value' => 'example'], $this->routeCollection->get('test_route_4')];
$this->routeProvider->expects($this->any())
->method('getRouteByName')
->will($this->returnValueMap($map));
$map = [];
$map[] = ['test_route_1', [], '/test-route-1'];
$map[] = ['test_route_2', [], '/test-route-2'];
$map[] = ['test_route_3', [], '/test-route-3'];
$map[] = ['test_route_4', ['value' => 'example'], '/test-route-4/example'];
$this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
$this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->argumentsResolverFactory = $this->getMock('Drupal\Core\Access\AccessArgumentsResolverFactoryInterface');
$this->checkProvider = new CheckProvider();
$this->checkProvider->setContainer($this->container);
$this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
}
/**
* Tests \Drupal\Core\Access\AccessManager::setChecks().
*/
public function testSetChecks() {
// Check setChecks without any access checker defined yet.
$this->checkProvider->setChecks($this->routeCollection);
foreach ($this->routeCollection->all() as $route) {
$this->assertNull($route->getOption('_access_checks'));
}
$this->setupAccessChecker();
$this->checkProvider->setChecks($this->routeCollection);
$this->assertEquals($this->routeCollection->get('test_route_1')->getOption('_access_checks'), NULL);
$this->assertEquals($this->routeCollection->get('test_route_2')->getOption('_access_checks'), ['test_access_default']);
$this->assertEquals($this->routeCollection->get('test_route_3')->getOption('_access_checks'), ['test_access_default']);
}
/**
* Tests setChecks with a dynamic access checker.
*/
public function testSetChecksWithDynamicAccessChecker() {
// Setup the access manager.
$this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
// Setup the dynamic access checker.
$access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
$this->container->set('test_access', $access_check);
$this->checkProvider->addCheckService('test_access', 'access');
$route = new Route('/test-path', [], ['_foo' => '1', '_bar' => '1']);
$route2 = new Route('/test-path', [], ['_foo' => '1', '_bar' => '2']);
$collection = new RouteCollection();
$collection->add('test_route', $route);
$collection->add('test_route2', $route2);
$access_check->expects($this->exactly(2))
->method('applies')
->with($this->isInstanceOf('Symfony\Component\Routing\Route'))
->will($this->returnCallback(function (Route $route) {
return $route->getRequirement('_bar') == 2;
}));
$this->checkProvider->setChecks($collection);
$this->assertEmpty($route->getOption('_access_checks'));
$this->assertEquals(['test_access'], $route2->getOption('_access_checks'));
}
/**
* Tests \Drupal\Core\Access\AccessManager::check().
*/
public function testCheck() {
$route_matches = [];
// Construct route match objects.
foreach ($this->routeCollection->all() as $route_name => $route) {
$route_matches[$route_name] = new RouteMatch($route_name, $route, [], []);
}
// Check route access without any access checker defined yet.
foreach ($route_matches as $route_match) {
$this->assertEquals(FALSE, $this->accessManager->check($route_match, $this->account));
$this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_match, $this->account, NULL, TRUE));
}
$this->setupAccessChecker();
// An access checker got setup, but the routes haven't been setup using
// setChecks.
foreach ($route_matches as $route_match) {
$this->assertEquals(FALSE, $this->accessManager->check($route_match, $this->account));
$this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_match, $this->account, NULL, TRUE));
}
// Now applicable access checks have been saved on each route object.
$this->checkProvider->setChecks($this->routeCollection);
$this->setupAccessArgumentsResolverFactory();
$this->assertEquals(FALSE, $this->accessManager->check($route_matches['test_route_1'], $this->account));
$this->assertEquals(TRUE, $this->accessManager->check($route_matches['test_route_2'], $this->account));
$this->assertEquals(FALSE, $this->accessManager->check($route_matches['test_route_3'], $this->account));
$this->assertEquals(TRUE, $this->accessManager->check($route_matches['test_route_4'], $this->account));
$this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_matches['test_route_1'], $this->account, NULL, TRUE));
$this->assertEquals(AccessResult::allowed(), $this->accessManager->check($route_matches['test_route_2'], $this->account, NULL, TRUE));
$this->assertEquals(AccessResult::forbidden(), $this->accessManager->check($route_matches['test_route_3'], $this->account, NULL, TRUE));
$this->assertEquals(AccessResult::allowed(), $this->accessManager->check($route_matches['test_route_4'], $this->account, NULL, TRUE));
}
/**
* Tests \Drupal\Core\Access\AccessManager::check() with no account specified.
*
* @covers ::check
*/
public function testCheckWithNullAccount() {
$this->setupAccessChecker();
$this->checkProvider->setChecks($this->routeCollection);
$route = $this->routeCollection->get('test_route_2');
$route_match = new RouteMatch('test_route_2', $route, [], []);
// Asserts that the current user is passed to the access arguments resolver
// factory.
$this->setupAccessArgumentsResolverFactory()
->with($route_match, $this->currentUser, NULL);
$this->assertTrue($this->accessManager->check($route_match));
}
/**
* Provides data for the conjunction test.
*
* @return array
* An array of data for check conjunctions.
*
* @see \Drupal\Tests\Core\Access\AccessManagerTest::testCheckConjunctions()
*/
public function providerTestCheckConjunctions() {
$access_allow = AccessResult::allowed();
$access_deny = AccessResult::neutral();
$access_kill = AccessResult::forbidden();
$access_configurations = [];
$access_configurations[] = [
'name' => 'test_route_4',
'condition_one' => 'TRUE',
'condition_two' => 'FALSE',
'expected' => $access_kill,
];
$access_configurations[] = [
'name' => 'test_route_5',
'condition_one' => 'TRUE',
'condition_two' => 'NULL',
'expected' => $access_deny,
];
$access_configurations[] = [
'name' => 'test_route_6',
'condition_one' => 'FALSE',
'condition_two' => 'NULL',
'expected' => $access_kill,
];
$access_configurations[] = [
'name' => 'test_route_7',
'condition_one' => 'TRUE',
'condition_two' => 'TRUE',
'expected' => $access_allow,
];
$access_configurations[] = [
'name' => 'test_route_8',
'condition_one' => 'FALSE',
'condition_two' => 'FALSE',
'expected' => $access_kill,
];
$access_configurations[] = [
'name' => 'test_route_9',
'condition_one' => 'NULL',
'condition_two' => 'NULL',
'expected' => $access_deny,
];
return $access_configurations;
}
/**
* Test \Drupal\Core\Access\AccessManager::check() with conjunctions.
*
* @dataProvider providerTestCheckConjunctions
*/
public function testCheckConjunctions($name, $condition_one, $condition_two, $expected_access) {
$this->setupAccessChecker();
$access_check = new DefinedTestAccessCheck();
$this->container->register('test_access_defined', $access_check);
$this->checkProvider->addCheckService('test_access_defined', 'access', ['_test_access']);
$route_collection = new RouteCollection();
// Setup a test route for each access configuration.
$requirements = [
'_access' => $condition_one,
'_test_access' => $condition_two,
];
$route = new Route($name, [], $requirements);
$route_collection->add($name, $route);
$this->checkProvider->setChecks($route_collection);
$this->setupAccessArgumentsResolverFactory();
$route_match = new RouteMatch($name, $route, [], []);
$this->assertEquals($expected_access->isAllowed(), $this->accessManager->check($route_match, $this->account));
$this->assertEquals($expected_access, $this->accessManager->check($route_match, $this->account, NULL, TRUE));
}
/**
* Tests the checkNamedRoute method.
*
* @see \Drupal\Core\Access\AccessManager::checkNamedRoute()
*/
public function testCheckNamedRoute() {
$this->setupAccessChecker();
$this->checkProvider->setChecks($this->routeCollection);
$this->setupAccessArgumentsResolverFactory();
$this->paramConverter->expects($this->at(0))
->method('convert')
->with([RouteObjectInterface::ROUTE_NAME => 'test_route_2', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_2')])
->will($this->returnValue([]));
$this->paramConverter->expects($this->at(1))
->method('convert')
->with([RouteObjectInterface::ROUTE_NAME => 'test_route_2', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_2')])
->will($this->returnValue([]));
$this->paramConverter->expects($this->at(2))
->method('convert')
->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_4', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_4')])
->will($this->returnValue(['value' => 'example']));
$this->paramConverter->expects($this->at(3))
->method('convert')
->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_4', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_4')])
->will($this->returnValue(['value' => 'example']));
// Tests the access with routes with parameters without given request.
$this->assertEquals(TRUE, $this->accessManager->checkNamedRoute('test_route_2', [], $this->account));
$this->assertEquals(AccessResult::allowed(), $this->accessManager->checkNamedRoute('test_route_2', [], $this->account, TRUE));
$this->assertEquals(TRUE, $this->accessManager->checkNamedRoute('test_route_4', ['value' => 'example'], $this->account));
$this->assertEquals(AccessResult::allowed(), $this->accessManager->checkNamedRoute('test_route_4', ['value' => 'example'], $this->account, TRUE));
}
/**
* Tests the checkNamedRoute with upcasted values.
*
* @see \Drupal\Core\Access\AccessManager::checkNamedRoute()
*/
public function testCheckNamedRouteWithUpcastedValues() {
$this->routeCollection = new RouteCollection();
$route = new Route('/test-route-1/{value}', [], ['_test_access' => 'TRUE']);
$this->routeCollection->add('test_route_1', $route);
$this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
$this->routeProvider->expects($this->any())
->method('getRouteByName')
->with('test_route_1', ['value' => 'example'])
->will($this->returnValue($route));
$map = [];
$map[] = ['test_route_1', ['value' => 'example'], '/test-route-1/example'];
$this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
$this->paramConverter->expects($this->atLeastOnce())
->method('convert')
->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_1', RouteObjectInterface::ROUTE_OBJECT => $route])
->will($this->returnValue(['value' => 'upcasted_value']));
$this->setupAccessArgumentsResolverFactory($this->exactly(2))
->with($this->callback(function ($route_match) {
return $route_match->getParameters()->get('value') == 'upcasted_value';
}));
$this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
$access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
$access_check->expects($this->atLeastOnce())
->method('applies')
->will($this->returnValue(TRUE));
$access_check->expects($this->atLeastOnce())
->method('access')
->will($this->returnValue(AccessResult::forbidden()));
$this->container->set('test_access', $access_check);
$this->checkProvider->addCheckService('test_access', 'access');
$this->checkProvider->setChecks($this->routeCollection);
$this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', ['value' => 'example'], $this->account));
$this->assertEquals(AccessResult::forbidden(), $this->accessManager->checkNamedRoute('test_route_1', ['value' => 'example'], $this->account, TRUE));
}
/**
* Tests the checkNamedRoute with default values.
*
* @covers ::checkNamedRoute
*/
public function testCheckNamedRouteWithDefaultValue() {
$this->routeCollection = new RouteCollection();
$route = new Route('/test-route-1/{value}', ['value' => 'example'], ['_test_access' => 'TRUE']);
$this->routeCollection->add('test_route_1', $route);
$this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
$this->routeProvider->expects($this->any())
->method('getRouteByName')
->with('test_route_1', [])
->will($this->returnValue($route));
$map = [];
$map[] = ['test_route_1', ['value' => 'example'], '/test-route-1/example'];
$this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
$this->paramConverter->expects($this->atLeastOnce())
->method('convert')
->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_1', RouteObjectInterface::ROUTE_OBJECT => $route])
->will($this->returnValue(['value' => 'upcasted_value']));
$this->setupAccessArgumentsResolverFactory($this->exactly(2))
->with($this->callback(function ($route_match) {
return $route_match->getParameters()->get('value') == 'upcasted_value';
}));
$this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
$access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
$access_check->expects($this->atLeastOnce())
->method('applies')
->will($this->returnValue(TRUE));
$access_check->expects($this->atLeastOnce())
->method('access')
->will($this->returnValue(AccessResult::forbidden()));
$this->container->set('test_access', $access_check);
$this->checkProvider->addCheckService('test_access', 'access');
$this->checkProvider->setChecks($this->routeCollection);
$this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', [], $this->account));
$this->assertEquals(AccessResult::forbidden(), $this->accessManager->checkNamedRoute('test_route_1', [], $this->account, TRUE));
}
/**
* Tests checkNamedRoute given an invalid/non existing route name.
*/
public function testCheckNamedRouteWithNonExistingRoute() {
$this->routeProvider->expects($this->any())
->method('getRouteByName')
->will($this->throwException(new RouteNotFoundException()));
$this->setupAccessChecker();
$this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', [], $this->account), 'A non existing route lead to access.');
$this->assertEquals(AccessResult::forbidden()->addCacheTags(['config:core.extension']), $this->accessManager->checkNamedRoute('test_route_1', [], $this->account, TRUE), 'A non existing route lead to access.');
}
/**
* Tests that an access checker throws an exception for not allowed values.
*
* @dataProvider providerCheckException
*/
public function testCheckException($return_value) {
$route_provider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
// Setup a test route for each access configuration.
$requirements = [
'_test_incorrect_value' => 'TRUE',
];
$options = [
'_access_checks' => [
'test_incorrect_value',
],
];
$route = new Route('', [], $requirements, $options);
$route_provider->expects($this->any())
->method('getRouteByName')
->will($this->returnValue($route));
$this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
$this->paramConverter->expects($this->any())
->method('convert')
->will($this->returnValue([]));
$this->setupAccessArgumentsResolverFactory();
$container = new ContainerBuilder();
// Register a service that will return an incorrect value.
$access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
$access_check->expects($this->any())
->method('access')
->will($this->returnValue($return_value));
$container->set('test_incorrect_value', $access_check);
$access_manager = new AccessManager($route_provider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
$this->checkProvider->setContainer($container);
$this->checkProvider->addCheckService('test_incorrect_value', 'access');
$this->setExpectedException(AccessException::class);
$access_manager->checkNamedRoute('test_incorrect_value', [], $this->account);
}
/**
* Data provider for testCheckException.
*
* @return array
*/
public function providerCheckException() {
return [
[[1]],
['string'],
[0],
[1],
];
}
/**
* Adds a default access check service to the container and the access manager.
*/
protected function setupAccessChecker() {
$access_check = new DefaultAccessCheck();
$this->container->register('test_access_default', $access_check);
$this->checkProvider->addCheckService('test_access_default', 'access', ['_access']);
}
/**
* Add default expectations to the access arguments resolver factory.
*/
protected function setupAccessArgumentsResolverFactory($constraint = NULL) {
if (!isset($constraint)) {
$constraint = $this->any();
}
return $this->argumentsResolverFactory->expects($constraint)
->method('getArgumentsResolver')
->will($this->returnCallback(function ($route_match, $account) {
$resolver = $this->getMock('Drupal\Component\Utility\ArgumentsResolverInterface');
$resolver->expects($this->any())
->method('getArguments')
->will($this->returnCallback(function ($callable) use ($route_match) {
return [$route_match->getRouteObject()];
}));
return $resolver;
}));
}
}
/**
* Defines an interface with a defined access() method for mocking.
*/
interface TestAccessCheckInterface extends AccessCheckInterface {
public function access();
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Access\AccessResultForbidden
* @group Access
*/
class AccessResultForbiddenTest extends UnitTestCase {
/**
* Tests the construction of an AccessResultForbidden object.
*
* @covers ::__construct
* @covers ::getReason
*/
public function testConstruction() {
$a = new AccessResultForbidden();
$this->assertEquals(NULL, $a->getReason());
$reason = $this->getRandomGenerator()->string();
$b = new AccessResultForbidden($reason);
$this->assertEquals($reason, $b->getReason());
}
/**
* Test setReason()
*
* @covers ::setReason
*/
public function testSetReason() {
$a = new AccessResultForbidden();
$reason = $this->getRandomGenerator()->string();
$return = $a->setReason($reason);
$this->assertSame($reason, $a->getReason());
$this->assertSame($a, $return);
}
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Access\AccessResultNeutral
* @group Access
*/
class AccessResultNeutralTest extends UnitTestCase {
/**
* Tests the construction of an AccessResultForbidden object.
*
* @covers ::__construct
* @covers ::getReason
*/
public function testConstruction() {
$a = new AccessResultNeutral();
$this->assertNull($a->getReason());
$reason = $this->getRandomGenerator()->string();
$b = new AccessResultNeutral($reason);
$this->assertEquals($reason, $b->getReason());
}
/**
* Test setReason()
*
* @covers ::setReason
*/
public function testSetReason() {
$a = new AccessResultNeutral();
$reason = $this->getRandomGenerator()->string();
$return = $a->setReason($reason);
$this->assertSame($reason, $a->getReason());
$this->assertSame($a, $return);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
<?php
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Access\AccessResult;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Drupal\Core\Access\CsrfAccessCheck;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Access\CsrfAccessCheck
* @group Access
*/
class CsrfAccessCheckTest extends UnitTestCase {
/**
* The mock CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit_Framework_MockObject_MockObject
*/
protected $csrfToken;
/**
* The access checker.
*
* @var \Drupal\Core\Access\CsrfAccessCheck
*/
protected $accessCheck;
/**
* The mock route match.
*
* @var \Drupal\Core\RouteMatch\RouteMatchInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $routeMatch;
protected function setUp() {
$this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
$this->accessCheck = new CsrfAccessCheck($this->csrfToken);
}
/**
* Tests the access() method with a valid token.
*/
public function testAccessTokenPass() {
$this->csrfToken->expects($this->once())
->method('validate')
->with('test_query', 'test-path/42')
->will($this->returnValue(TRUE));
$this->routeMatch->expects($this->once())
->method('getRawParameters')
->will($this->returnValue(['node' => 42]));
$route = new Route('/test-path/{node}', [], ['_csrf_token' => 'TRUE']);
$request = Request::create('/test-path/42?token=test_query');
$this->assertEquals(AccessResult::allowed()->setCacheMaxAge(0), $this->accessCheck->access($route, $request, $this->routeMatch));
}
/**
* @covers ::access
*/
public function testCsrfTokenInvalid() {
$this->csrfToken->expects($this->once())
->method('validate')
->with('test_query', 'test-path')
->will($this->returnValue(FALSE));
$this->routeMatch->expects($this->once())
->method('getRawParameters')
->will($this->returnValue([]));
$route = new Route('/test-path', [], ['_csrf_token' => 'TRUE']);
$request = Request::create('/test-path?token=test_query');
$this->assertEquals(AccessResult::forbidden("'csrf_token' URL query argument is invalid.")->setCacheMaxAge(0), $this->accessCheck->access($route, $request, $this->routeMatch));
}
/**
* @covers ::access
*/
public function testCsrfTokenMissing() {
$this->csrfToken->expects($this->once())
->method('validate')
->with('', 'test-path')
->will($this->returnValue(FALSE));
$this->routeMatch->expects($this->once())
->method('getRawParameters')
->will($this->returnValue([]));
$route = new Route('/test-path', [], ['_csrf_token' => 'TRUE']);
$request = Request::create('/test-path');
$this->assertEquals(AccessResult::forbidden("'csrf_token' URL query argument is missing.")->setCacheMaxAge(0), $this->accessCheck->access($route, $request, $this->routeMatch));
}
}
@@ -0,0 +1,210 @@
<?php
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Site\Settings;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Component\Utility\Crypt;
/**
* Tests the CsrfTokenGenerator class.
*
* @group Access
* @coversDefaultClass \Drupal\Core\Access\CsrfTokenGenerator
*/
class CsrfTokenGeneratorTest extends UnitTestCase {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $generator;
/**
* The mock private key instance.
*
* @var \Drupal\Core\PrivateKey|\PHPUnit_Framework_MockObject_MockObject
*/
protected $privateKey;
/**
* The mock session metadata bag.
*
* @var \Drupal\Core\Session\MetadataBag|\PHPUnit_Framework_MockObject_MockObject
*/
protected $sessionMetadata;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->privateKey = $this->getMockBuilder('Drupal\Core\PrivateKey')
->disableOriginalConstructor()
->setMethods(['get'])
->getMock();
$this->sessionMetadata = $this->getMockBuilder('Drupal\Core\Session\MetadataBag')
->disableOriginalConstructor()
->getMock();
$settings = [
'hash_salt' => $this->randomMachineName(),
];
new Settings($settings);
$this->generator = new CsrfTokenGenerator($this->privateKey, $this->sessionMetadata);
}
/**
* Set up default expectations on the mocks.
*/
protected function setupDefaultExpectations() {
$key = Crypt::randomBytesBase64();
$this->privateKey->expects($this->any())
->method('get')
->will($this->returnValue($key));
$seed = Crypt::randomBytesBase64();
$this->sessionMetadata->expects($this->any())
->method('getCsrfTokenSeed')
->will($this->returnValue($seed));
}
/**
* Tests CsrfTokenGenerator::get().
*
* @covers ::get
*/
public function testGet() {
$this->setupDefaultExpectations();
$this->assertInternalType('string', $this->generator->get());
$this->assertNotSame($this->generator->get(), $this->generator->get($this->randomMachineName()));
$this->assertNotSame($this->generator->get($this->randomMachineName()), $this->generator->get($this->randomMachineName()));
}
/**
* Tests that a new token seed is generated upon first use.
*
* @covers ::get
*/
public function testGenerateSeedOnGet() {
$key = Crypt::randomBytesBase64();
$this->privateKey->expects($this->any())
->method('get')
->will($this->returnValue($key));
$this->sessionMetadata->expects($this->once())
->method('getCsrfTokenSeed')
->will($this->returnValue(NULL));
$this->sessionMetadata->expects($this->once())
->method('setCsrfTokenSeed')
->with($this->isType('string'));
$this->assertInternalType('string', $this->generator->get());
}
/**
* Tests CsrfTokenGenerator::validate().
*
* @covers ::validate
*/
public function testValidate() {
$this->setupDefaultExpectations();
$token = $this->generator->get();
$this->assertTrue($this->generator->validate($token));
$this->assertFalse($this->generator->validate($token, 'foo'));
$token = $this->generator->get('bar');
$this->assertTrue($this->generator->validate($token, 'bar'));
}
/**
* Tests CsrfTokenGenerator::validate() with different parameter types.
*
* @param mixed $token
* The token to be validated.
* @param mixed $value
* (optional) An additional value to base the token on.
*
* @covers ::validate
* @dataProvider providerTestValidateParameterTypes
*/
public function testValidateParameterTypes($token, $value) {
$this->setupDefaultExpectations();
// The following check might throw PHP fatals and notices, so we disable
// error assertions.
set_error_handler(function () {return TRUE;});
$this->assertFalse($this->generator->validate($token, $value));
restore_error_handler();
}
/**
* Provides data for testValidateParameterTypes.
*
* @return array
* An array of data used by the test.
*/
public function providerTestValidateParameterTypes() {
return [
[[], ''],
[TRUE, 'foo'],
[0, 'foo'],
];
}
/**
* Tests CsrfTokenGenerator::validate() with invalid parameter types.
*
* @param mixed $token
* The token to be validated.
* @param mixed $value
* (optional) An additional value to base the token on.
*
* @covers ::validate
* @dataProvider providerTestInvalidParameterTypes
*/
public function testInvalidParameterTypes($token, $value = '') {
$this->setupDefaultExpectations();
$this->setExpectedException(\InvalidArgumentException::class);
$this->generator->validate($token, $value);
}
/**
* Provides data for testInvalidParameterTypes.
*
* @return array
* An array of data used by the test.
*/
public function providerTestInvalidParameterTypes() {
return [
[NULL, new \stdClass()],
[0, []],
['', []],
[[], []],
];
}
/**
* Tests the exception thrown when no 'hash_salt' is provided in settings.
*
* @covers ::get
*/
public function testGetWithNoHashSalt() {
// Update settings with no hash salt.
new Settings([]);
$generator = new CsrfTokenGenerator($this->privateKey, $this->sessionMetadata);
$this->setExpectedException(\RuntimeException::class);
$generator->get();
}
}
@@ -0,0 +1,125 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Access\CustomAccessCheckTest.
*/
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\CustomAccessCheck;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\Core\Access\CustomAccessCheck
* @group Access
*/
class CustomAccessCheckTest extends UnitTestCase {
/**
* The access checker to test.
*
* @var \Drupal\Core\Access\CustomAccessCheck
*/
protected $accessChecker;
/**
* The mocked controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $controllerResolver;
/**
* The mocked arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $argumentsResolverFactory;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
$this->argumentsResolverFactory = $this->getMock('Drupal\Core\Access\AccessArgumentsResolverFactoryInterface');
$this->accessChecker = new CustomAccessCheck($this->controllerResolver, $this->argumentsResolverFactory);
}
/**
* Test the access method.
*/
public function testAccess() {
$route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
$this->controllerResolver->expects($this->at(0))
->method('getControllerFromDefinition')
->with('\Drupal\Tests\Core\Access\TestController::accessDeny')
->will($this->returnValue([new TestController(), 'accessDeny']));
$resolver0 = $this->getMock('Drupal\Component\Utility\ArgumentsResolverInterface');
$resolver0->expects($this->once())
->method('getArguments')
->will($this->returnValue([]));
$this->argumentsResolverFactory->expects($this->at(0))
->method('getArgumentsResolver')
->will($this->returnValue($resolver0));
$this->controllerResolver->expects($this->at(1))
->method('getControllerFromDefinition')
->with('\Drupal\Tests\Core\Access\TestController::accessAllow')
->will($this->returnValue([new TestController(), 'accessAllow']));
$resolver1 = $this->getMock('Drupal\Component\Utility\ArgumentsResolverInterface');
$resolver1->expects($this->once())
->method('getArguments')
->will($this->returnValue([]));
$this->argumentsResolverFactory->expects($this->at(1))
->method('getArgumentsResolver')
->will($this->returnValue($resolver1));
$this->controllerResolver->expects($this->at(2))
->method('getControllerFromDefinition')
->with('\Drupal\Tests\Core\Access\TestController::accessParameter')
->will($this->returnValue([new TestController(), 'accessParameter']));
$resolver2 = $this->getMock('Drupal\Component\Utility\ArgumentsResolverInterface');
$resolver2->expects($this->once())
->method('getArguments')
->will($this->returnValue(['parameter' => 'TRUE']));
$this->argumentsResolverFactory->expects($this->at(2))
->method('getArgumentsResolver')
->will($this->returnValue($resolver2));
$route = new Route('/test-route', [], ['_custom_access' => '\Drupal\Tests\Core\Access\TestController::accessDeny']);
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->assertEquals(AccessResult::neutral(), $this->accessChecker->access($route, $route_match, $account));
$route = new Route('/test-route', [], ['_custom_access' => '\Drupal\Tests\Core\Access\TestController::accessAllow']);
$this->assertEquals(AccessResult::allowed(), $this->accessChecker->access($route, $route_match, $account));
$route = new Route('/test-route', ['parameter' => 'TRUE'], ['_custom_access' => '\Drupal\Tests\Core\Access\TestController::accessParameter']);
$this->assertEquals(AccessResult::allowed(), $this->accessChecker->access($route, $route_match, $account));
}
}
class TestController {
public function accessAllow() {
return AccessResult::allowed();
}
public function accessDeny() {
return AccessResult::neutral();
}
public function accessParameter($parameter) {
return AccessResult::allowedIf($parameter == 'TRUE');
}
}
@@ -0,0 +1,57 @@
<?php
namespace Drupal\Tests\Core\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\DefaultAccessCheck;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\Core\Access\DefaultAccessCheck
* @group Access
*/
class DefaultAccessCheckTest extends UnitTestCase {
/**
* The access checker to test.
*
* @var \Drupal\Core\Access\DefaultAccessCheck
*/
protected $accessChecker;
/**
* The mocked account.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->accessChecker = new DefaultAccessCheck();
}
/**
* Test the access method.
*/
public function testAccess() {
$request = new Request([]);
$route = new Route('/test-route', [], ['_access' => 'NULL']);
$this->assertEquals(AccessResult::neutral(), $this->accessChecker->access($route, $request, $this->account));
$route = new Route('/test-route', [], ['_access' => 'FALSE']);
$this->assertEquals(AccessResult::forbidden(), $this->accessChecker->access($route, $request, $this->account));
$route = new Route('/test-route', [], ['_access' => 'TRUE']);
$this->assertEquals(AccessResult::allowed(), $this->accessChecker->access($route, $request, $this->account));
}
}
@@ -0,0 +1,118 @@
<?php
namespace Drupal\Tests\Core\Access;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Access\RouteProcessorCsrf;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\Core\Access\RouteProcessorCsrf
* @group Access
*/
class RouteProcessorCsrfTest extends UnitTestCase {
/**
* The mock CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit_Framework_MockObject_MockObject
*/
protected $csrfToken;
/**
* The route processor.
*
* @var \Drupal\Core\Access\RouteProcessorCsrf
*/
protected $processor;
protected function setUp() {
$this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$this->processor = new RouteProcessorCsrf($this->csrfToken);
}
/**
* Tests the processOutbound() method with no _csrf_token route requirement.
*/
public function testProcessOutboundNoRequirement() {
$this->csrfToken->expects($this->never())
->method('get');
$route = new Route('/test-path');
$parameters = [];
$bubbleable_metadata = new BubbleableMetadata();
$this->processor->processOutbound('test', $route, $parameters, $bubbleable_metadata);
// No parameters should be added to the parameters array.
$this->assertEmpty($parameters);
// Cacheability of routes without a _csrf_token route requirement is
// unaffected.
$this->assertEquals((new BubbleableMetadata()), $bubbleable_metadata);
}
/**
* Tests the processOutbound() method with a _csrf_token route requirement.
*/
public function testProcessOutbound() {
$route = new Route('/test-path', [], ['_csrf_token' => 'TRUE']);
$parameters = [];
$bubbleable_metadata = new BubbleableMetadata();
$this->processor->processOutbound('test', $route, $parameters, $bubbleable_metadata);
// 'token' should be added to the parameters array.
$this->assertArrayHasKey('token', $parameters);
// Bubbleable metadata of routes with a _csrf_token route requirement is a
// placeholder.
$path = 'test-path';
$placeholder = Crypt::hashBase64($path);
$placeholder_render_array = [
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
];
$this->assertSame($parameters['token'], $placeholder);
$this->assertEquals((new BubbleableMetadata())->setAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]), $bubbleable_metadata);
}
/**
* Tests the processOutbound() method with a dynamic path and one replacement.
*/
public function testProcessOutboundDynamicOne() {
$route = new Route('/test-path/{slug}', [], ['_csrf_token' => 'TRUE']);
$parameters = ['slug' => 100];
$bubbleable_metadata = new BubbleableMetadata();
$this->processor->processOutbound('test', $route, $parameters, $bubbleable_metadata);
// Bubbleable metadata of routes with a _csrf_token route requirement is a
// placeholder.
$path = 'test-path/100';
$placeholder = Crypt::hashBase64($path);
$placeholder_render_array = [
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
];
$this->assertEquals((new BubbleableMetadata())->setAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]), $bubbleable_metadata);
}
/**
* Tests the processOutbound() method with two parameter replacements.
*/
public function testProcessOutboundDynamicTwo() {
$route = new Route('{slug_1}/test-path/{slug_2}', [], ['_csrf_token' => 'TRUE']);
$parameters = ['slug_1' => 100, 'slug_2' => 'test'];
$bubbleable_metadata = new BubbleableMetadata();
$this->processor->processOutbound('test', $route, $parameters, $bubbleable_metadata);
// Bubbleable metadata of routes with a _csrf_token route requirement is a
// placeholder.
$path = '100/test-path/test';
$placeholder = Crypt::hashBase64($path);
$placeholder_render_array = [
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
];
$this->assertEquals((new BubbleableMetadata())->setAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]), $bubbleable_metadata);
}
}
@@ -0,0 +1,432 @@
<?php
namespace Drupal\Tests\Core\Ajax;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AfterCommand;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\BeforeCommand;
use Drupal\Core\Ajax\ChangedCommand;
use Drupal\Core\Ajax\CssCommand;
use Drupal\Core\Ajax\DataCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\RestripeCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\SetDialogOptionCommand;
use Drupal\Core\Ajax\SetDialogTitleCommand;
use Drupal\Core\Ajax\RedirectCommand;
/**
* Test coverage for various classes in the \Drupal\Core\Ajax namespace.
*
* @group Ajax
*/
class AjaxCommandsTest extends UnitTestCase {
/**
* @covers \Drupal\Core\Ajax\AddCssCommand
*/
public function testAddCssCommand() {
$command = new AddCssCommand('p{ text-decoration:blink; }');
$expected = [
'command' => 'add_css',
'data' => 'p{ text-decoration:blink; }',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\AfterCommand
*/
public function testAfterCommand() {
$command = new AfterCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => 'after',
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\AlertCommand
*/
public function testAlertCommand() {
$command = new AlertCommand('Set condition 1 throughout the ship!');
$expected = [
'command' => 'alert',
'text' => 'Set condition 1 throughout the ship!',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\AppendCommand
*/
public function testAppendCommand() {
$command = new AppendCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => 'append',
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\BeforeCommand
*/
public function testBeforeCommand() {
$command = new BeforeCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => 'before',
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\ChangedCommand
*/
public function testChangedCommand() {
$command = new ChangedCommand('#page-title', '#page-title-changed');
$expected = [
'command' => 'changed',
'selector' => '#page-title',
'asterisk' => '#page-title-changed',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\CssCommand
*/
public function testCssCommand() {
$command = new CssCommand('#page-title', ['text-decoration' => 'blink']);
$command->setProperty('font-size', '40px')->setProperty('font-weight', 'bold');
$expected = [
'command' => 'css',
'selector' => '#page-title',
'argument' => [
'text-decoration' => 'blink',
'font-size' => '40px',
'font-weight' => 'bold',
],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\DataCommand
*/
public function testDataCommand() {
$command = new DataCommand('#page-title', 'my-data', ['key' => 'value']);
$expected = [
'command' => 'data',
'selector' => '#page-title',
'name' => 'my-data',
'value' => ['key' => 'value'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\HtmlCommand
*/
public function testHtmlCommand() {
$command = new HtmlCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => 'html',
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\InsertCommand
*/
public function testInsertCommand() {
$command = new InsertCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => NULL,
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\InvokeCommand
*/
public function testInvokeCommand() {
$command = new InvokeCommand('#page-title', 'myMethod', ['var1', 'var2']);
$expected = [
'command' => 'invoke',
'selector' => '#page-title',
'method' => 'myMethod',
'args' => ['var1', 'var2'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\PrependCommand
*/
public function testPrependCommand() {
$command = new PrependCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => 'prepend',
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\RemoveCommand
*/
public function testRemoveCommand() {
$command = new RemoveCommand('#page-title');
$expected = [
'command' => 'remove',
'selector' => '#page-title',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\ReplaceCommand
*/
public function testReplaceCommand() {
$command = new ReplaceCommand('#page-title', '<p>New Text!</p>', ['my-setting' => 'setting']);
$expected = [
'command' => 'insert',
'method' => 'replaceWith',
'selector' => '#page-title',
'data' => '<p>New Text!</p>',
'settings' => ['my-setting' => 'setting'],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\RestripeCommand
*/
public function testRestripeCommand() {
$command = new RestripeCommand('#page-title');
$expected = [
'command' => 'restripe',
'selector' => '#page-title',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\SettingsCommand
*/
public function testSettingsCommand() {
$command = new SettingsCommand(['key' => 'value'], TRUE);
$expected = [
'command' => 'settings',
'settings' => ['key' => 'value'],
'merge' => TRUE,
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\OpenDialogCommand
*/
public function testOpenDialogCommand() {
$command = $this->getMockBuilder('Drupal\Core\Ajax\OpenDialogCommand')
->setConstructorArgs([
'#some-dialog', 'Title', '<p>Text!</p>', [
'url' => FALSE,
'width' => 500,
],
])
->setMethods(['getRenderedContent'])
->getMock();
// This method calls the render service, which isn't available. We want it
// to do nothing so we mock it to return a known value.
$command->expects($this->once())
->method('getRenderedContent')
->willReturn('rendered content');
$expected = [
'command' => 'openDialog',
'selector' => '#some-dialog',
'settings' => NULL,
'data' => 'rendered content',
'dialogOptions' => [
'url' => FALSE,
'width' => 500,
'title' => 'Title',
'modal' => FALSE,
],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\OpenModalDialogCommand
*/
public function testOpenModalDialogCommand() {
$command = $this->getMockBuilder('Drupal\Core\Ajax\OpenModalDialogCommand')
->setConstructorArgs([
'Title', '<p>Text!</p>', [
'url' => 'example',
'width' => 500,
],
])
->setMethods(['getRenderedContent'])
->getMock();
// This method calls the render service, which isn't available. We want it
// to do nothing so we mock it to return a known value.
$command->expects($this->once())
->method('getRenderedContent')
->willReturn('rendered content');
$expected = [
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'data' => 'rendered content',
'dialogOptions' => [
'url' => 'example',
'width' => 500,
'title' => 'Title',
'modal' => TRUE,
],
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\CloseModalDialogCommand
*/
public function testCloseModalDialogCommand() {
$command = new CloseModalDialogCommand();
$expected = [
'command' => 'closeDialog',
'selector' => '#drupal-modal',
'persist' => FALSE,
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\CloseDialogCommand
*/
public function testCloseDialogCommand() {
$command = new CloseDialogCommand('#some-dialog', TRUE);
$expected = [
'command' => 'closeDialog',
'selector' => '#some-dialog',
'persist' => TRUE,
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\SetDialogOptionCommand
*/
public function testSetDialogOptionCommand() {
$command = new SetDialogOptionCommand('#some-dialog', 'width', '500');
$expected = [
'command' => 'setDialogOption',
'selector' => '#some-dialog',
'optionName' => 'width',
'optionValue' => '500',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\SetDialogTitleCommand
*/
public function testSetDialogTitleCommand() {
$command = new SetDialogTitleCommand('#some-dialog', 'Example');
$expected = [
'command' => 'setDialogOption',
'selector' => '#some-dialog',
'optionName' => 'title',
'optionValue' => 'Example',
];
$this->assertEquals($expected, $command->render());
}
/**
* @covers \Drupal\Core\Ajax\RedirectCommand
*/
public function testRedirectCommand() {
$command = new RedirectCommand('http://example.com');
$expected = [
'command' => 'redirect',
'url' => 'http://example.com',
];
$this->assertEquals($expected, $command->render());
}
}
@@ -0,0 +1,94 @@
<?php
namespace Drupal\Tests\Core\Ajax;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* @coversDefaultClass \Drupal\Core\Ajax\AjaxResponse
* @group Ajax
*/
class AjaxResponseTest extends UnitTestCase {
/**
* The tested ajax response object.
*
* @var \Drupal\Core\Ajax\AjaxResponse
*/
protected $ajaxResponse;
protected function setUp() {
$this->ajaxResponse = new AjaxResponse();
}
/**
* Tests the add and getCommands method.
*
* @see \Drupal\Core\Ajax\AjaxResponse::addCommand()
* @see \Drupal\Core\Ajax\AjaxResponse::getCommands()
*/
public function testCommands() {
$command_one = $this->getMock('Drupal\Core\Ajax\CommandInterface');
$command_one->expects($this->once())
->method('render')
->will($this->returnValue(['command' => 'one']));
$command_two = $this->getMock('Drupal\Core\Ajax\CommandInterface');
$command_two->expects($this->once())
->method('render')
->will($this->returnValue(['command' => 'two']));
$command_three = $this->getMock('Drupal\Core\Ajax\CommandInterface');
$command_three->expects($this->once())
->method('render')
->will($this->returnValue(['command' => 'three']));
$this->ajaxResponse->addCommand($command_one);
$this->ajaxResponse->addCommand($command_two);
$this->ajaxResponse->addCommand($command_three, TRUE);
// Ensure that the added commands are in the right order.
$commands =& $this->ajaxResponse->getCommands();
$this->assertSame($commands[1], ['command' => 'one']);
$this->assertSame($commands[2], ['command' => 'two']);
$this->assertSame($commands[0], ['command' => 'three']);
// Remove one and change one element from commands and ensure the reference
// worked as expected.
unset($commands[2]);
$commands[0]['class'] = 'test-class';
$commands = $this->ajaxResponse->getCommands();
$this->assertSame($commands[1], ['command' => 'one']);
$this->assertFalse(isset($commands[2]));
$this->assertSame($commands[0], ['command' => 'three', 'class' => 'test-class']);
}
/**
* Tests the support for IE specific headers in file uploads.
*
* @cover ::prepareResponse
*/
public function testPrepareResponseForIeFormRequestsWithFileUpload() {
$request = Request::create('/example', 'POST');
$request->headers->set('Accept', 'text/html');
$response = new AjaxResponse([]);
$response->headers->set('Content-Type', 'application/json; charset=utf-8');
$ajax_response_attachments_processor = $this->getMock('\Drupal\Core\Render\AttachmentsResponseProcessorInterface');
$subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
$event = new FilterResponseEvent(
$this->getMock('\Symfony\Component\HttpKernel\HttpKernelInterface'),
$request,
HttpKernelInterface::MASTER_REQUEST,
$response
);
$subscriber->onResponse($event);
$this->assertEquals('text/html; charset=utf-8', $response->headers->get('Content-Type'));
$this->assertEquals($response->getContent(), '<textarea>[]</textarea>');
}
}
@@ -0,0 +1,66 @@
<?php
namespace Drupal\Tests\Core\Annotation;
use Drupal\Core\Annotation\PluralTranslation;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Annotation\PluralTranslation
* @group Annotation
*/
class PluralTranslationTest extends UnitTestCase {
/**
* @covers ::get
*
* @dataProvider providerTestGet
*/
public function testGet(array $values) {
$annotation = new PluralTranslation($values);
$default_values = [
'context' => NULL,
];
$this->assertEquals($values + $default_values, $annotation->get());
}
/**
* Provides data to self::testGet().
*/
public function providerTestGet() {
$data = [];
$data[] = [
[
'singular' => $this->randomMachineName(),
'plural' => $this->randomMachineName(),
'context' => $this->randomMachineName(),
],
];
$data[] = [
[
'singular' => $this->randomMachineName(),
'plural' => $this->randomMachineName(),
],
];
return $data;
}
/**
* @dataProvider providerTestMissingData
*/
public function testMissingData($data) {
$this->setExpectedException(\InvalidArgumentException::class);
new PluralTranslation($data);
}
public function providerTestMissingData() {
$data = [];
$data['all-missing'] = [[]];
$data['singular-missing'] = [['plural' => 'muh']];
$data['plural-missing'] = [['singular' => 'muh']];
return $data;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\Tests\Core\Annotation;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Annotation\Translation
* @group Annotation
*/
class TranslationTest extends UnitTestCase {
/**
* The translation manager used for testing.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $translationManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->translationManager = $this->getStringTranslationStub();
}
/**
* @covers ::get
*
* @dataProvider providerTestGet
*/
public function testGet(array $values, $expected) {
$container = new ContainerBuilder();
$container->set('string_translation', $this->translationManager);
\Drupal::setContainer($container);
$arguments = isset($values['arguments']) ? $values['arguments'] : [];
$options = isset($values['context']) ? [
'context' => $values['context'],
] : [];
$annotation = new Translation($values);
$this->assertSame($expected, (string) $annotation->get());
}
/**
* Provides data to self::testGet().
*/
public function providerTestGet() {
$data = [];
$data[] = [
[
'value' => 'Foo',
],
'Foo'
];
$random = $this->randomMachineName();
$random_html_entity = '&' . $random;
$data[] = [
[
'value' => 'Foo @bar @baz %qux',
'arguments' => [
'@bar' => $random,
'@baz' => $random_html_entity,
'%qux' => $random_html_entity,
],
'context' => $this->randomMachineName(),
],
'Foo ' . $random . ' &amp;' . $random . ' <em class="placeholder">&amp;' . $random . '</em>',
];
return $data;
}
}
@@ -0,0 +1,184 @@
<?php
namespace Drupal\Tests\Core\Assert;
use Behat\Mink\Element\DocumentElement;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Session;
use Drupal\Component\Render\MarkupInterface;
use Drupal\FunctionalTests\AssertLegacyTrait;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\WebAssert;
use PHPUnit_Framework_ExpectationFailedException;
/**
* @coversDefaultClass \Drupal\FunctionalTests\AssertLegacyTrait
* @group Assert
*/
class AssertLegacyTraitTest extends UnitTestCase {
use AssertLegacyTrait;
/**
* The mocked Mink session object used for testing.
*
* @var \Behat\Mink\Session|\Prophecy\Prophecy\ObjectProphecy
*/
protected $session;
/**
* The mocked page element used for testing.
*
* @var Behat\Mink\Element\DocumentElement|\Prophecy\Prophecy\ObjectProphecy
*/
protected $page;
/**
* The mocked web assert class.
*
* @var \Drupal\Tests\WebAssert|\Prophecy\Prophecy\ObjectProphecy
*/
protected $webAssert;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->page = $this->prophesize(DocumentElement::class);
$this->session = $this->prophesize(Session::class);
$this->session->getPage()->willReturn($this->page->reveal());
$this->webAssert = $this->prophesize(WebAssert::class);
}
/**
* @covers ::assertUniqueText
*/
public function testAssertUniqueText() {
$this->page->getText()->willReturn('foo bar bar');
$this->assertUniqueText('foo');
}
/**
* @covers ::assertUniqueText
*/
public function testAssertUniqueTextFail() {
$this->page->getText()->willReturn('foo bar bar');
$this->setExpectedException(PHPUnit_Framework_ExpectationFailedException::class);
$this->assertUniqueText('bar');
}
/**
* @covers ::assertUniqueText
*/
public function testAssertUniqueTextUnknown() {
$this->page->getText()->willReturn('foo bar bar');
$this->setExpectedException(PHPUnit_Framework_ExpectationFailedException::class);
$this->assertUniqueText('alice');
}
/**
* @covers ::assertUniqueText
*/
public function testAssertUniqueTextMarkup() {
$this->page->getText()->willReturn('foo bar bar');
$markupObject = $this->prophesize(MarkupInterface::class);
$markupObject->__toString()->willReturn('foo');
$this->assertUniqueText($markupObject->reveal());
}
/**
* @covers ::assertNoUniqueText
*/
public function testAssertNoUniqueText() {
$this->page->getText()->willReturn('foo bar bar');
$this->assertNoUniqueText('bar');
}
/**
* @covers ::assertNoUniqueText
*/
public function testAssertNoUniqueTextFail() {
$this->page->getText()->willReturn('foo bar bar');
$this->setExpectedException(PHPUnit_Framework_ExpectationFailedException::class);
$this->assertNoUniqueText('foo');
}
/**
* @covers ::assertNoUniqueText
*/
public function testAssertNoUniqueTextUnknown() {
$this->page->getText()->willReturn('foo bar bar');
$this->setExpectedException(PHPUnit_Framework_ExpectationFailedException::class);
$this->assertNoUniqueText('alice');
}
/**
* @covers ::assertNoUniqueText
*/
public function testAssertNoUniqueTextMarkup() {
$this->page->getText()->willReturn('foo bar bar');
$markupObject = $this->prophesize(MarkupInterface::class);
$markupObject->__toString()->willReturn('bar');
$this->assertNoUniqueText($markupObject->reveal());
}
/**
* @covers ::assertOptionSelected
*/
public function testAssertOptionSelected() {
$option_field = $this->prophesize(NodeElement::class);
$option_field->hasAttribute('selected')->willReturn(TRUE);
$this->webAssert
->optionExists('myselect', 'two')
->willReturn($option_field->reveal());
$this->assertOptionSelected('myselect', 'two');
}
/**
* @covers ::assertOptionSelected
*/
public function testAssertOptionSelectedFail() {
$option_field = $this->prophesize(NodeElement::class);
$option_field->hasAttribute('selected')->willReturn(FALSE);
$this->webAssert
->optionExists('myselect', 'two')
->willReturn($option_field->reveal());
$this->setExpectedException(PHPUnit_Framework_ExpectationFailedException::class);
$this->assertOptionSelected('myselect', 'two');
}
/**
* @covers ::assertNoPattern
*/
public function testAssertNoPattern() {
$this->webAssert
->responseNotMatches('/.*foo$/')
->shouldBeCalled();
$this->assertNoPattern('/.*foo$/');
}
/**
* Returns a mocked behat session object.
*
* @return \Behat\Mink\Session
* The mocked session.
*/
protected function getSession() {
return $this->session->reveal();
}
/**
* {@inheritdoc}
*/
public function assertSession($name = NULL) {
return $this->webAssert->reveal();
}
}
@@ -0,0 +1,169 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Asset\AssetResolverTest.
*/
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\AssetResolver;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Asset\AssetResolver
* @group Asset
*/
class AssetResolverTest extends UnitTestCase {
/**
* The tested asset resolver service.
*
* @var \Drupal\Core\Asset\AssetResolver
*/
protected $assetResolver;
/**
* The mocked library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $libraryDiscovery;
/**
* The mocked library dependency resolver.
*
* @var \Drupal\Core\Asset\LibraryDependencyResolverInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $libraryDependencyResolver;
/**
* The mocked module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $moduleHandler;
/**
* The mocked theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $themeManager;
/**
* The mocked language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* The cache backend to use.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->libraryDiscovery = $this->getMockBuilder('Drupal\Core\Asset\LibraryDiscovery')
->disableOriginalConstructor()
->getMock();
$this->libraryDependencyResolver = $this->getMock('\Drupal\Core\Asset\LibraryDependencyResolverInterface');
$this->libraryDependencyResolver->expects($this->any())
->method('getLibrariesWithDependencies')
->willReturnArgument(0);
$this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
$this->themeManager = $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface');
$active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
->disableOriginalConstructor()
->getMock();
$active_theme->expects($this->any())
->method('getName')
->willReturn('bartik');
$this->themeManager->expects($this->any())
->method('getActiveTheme')
->willReturn($active_theme);
$this->languageManager = $this->getMock('\Drupal\Core\Language\LanguageManagerInterface');
$english = $this->getMock('\Drupal\Core\Language\LanguageInterface');
$english->expects($this->any())
->method('getId')
->willReturn('en');
$japanese = $this->getMock('\Drupal\Core\Language\LanguageInterface');
$japanese->expects($this->any())
->method('getId')
->willReturn('jp');
$this->languageManager = $this->getMock('\Drupal\Core\Language\LanguageManagerInterface');
$this->languageManager->expects($this->any())
->method('getCurrentLanguage')
->will($this->onConsecutiveCalls($english, $english, $japanese, $japanese));
$this->cache = new TestMemoryBackend();
$this->assetResolver = new AssetResolver($this->libraryDiscovery, $this->libraryDependencyResolver, $this->moduleHandler, $this->themeManager, $this->languageManager, $this->cache);
}
/**
* @covers ::getCssAssets
* @dataProvider providerAttachedAssets
*/
public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) {
$this->assetResolver->getCssAssets($assets_a, FALSE);
$this->assetResolver->getCssAssets($assets_b, FALSE);
$this->assertCount($expected_cache_item_count, $this->cache->getAllCids());
}
/**
* @covers ::getJsAssets
* @dataProvider providerAttachedAssets
*/
public function testGetJsAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) {
$this->assetResolver->getJsAssets($assets_a, FALSE);
$this->assetResolver->getJsAssets($assets_b, FALSE);
$this->assertCount($expected_cache_item_count, $this->cache->getAllCids());
$this->assetResolver->getJsAssets($assets_a, FALSE);
$this->assetResolver->getJsAssets($assets_b, FALSE);
$this->assertCount($expected_cache_item_count * 2, $this->cache->getAllCids());
}
public function providerAttachedAssets() {
$time = time();
return [
'same libraries, different timestamps' => [
(new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal'])->setSettings(['currentTime' => $time]),
(new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal'])->setSettings(['currentTime' => $time + 100]),
1
],
'different libraries, same timestamps' => [
(new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal'])->setSettings(['currenttime' => $time]),
(new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal', 'core/jquery'])->setSettings(['currentTime' => $time]),
2
],
];
}
}
if (!defined('CSS_AGGREGATE_DEFAULT')) {
define('CSS_AGGREGATE_DEFAULT', 0);
}
if (!defined('JS_DEFAULT')) {
define('JS_DEFAULT', 0);
}
class TestMemoryBackend extends MemoryBackend {
public function getAllCids() {
return array_keys($this->cache);
}
}
@@ -0,0 +1,156 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\CssCollectionGrouper;
use Drupal\Tests\UnitTestCase;
/**
* Tests the CSS asset collection grouper.
*
* @group Asset
*/
class CssCollectionGrouperUnitTest extends UnitTestCase {
/**
* A CSS asset grouper.
*
* @var \Drupal\Core\Asset\CssCollectionGrouper object.
*/
protected $grouper;
protected function setUp() {
parent::setUp();
$this->grouper = new CssCollectionGrouper();
}
/**
* Tests \Drupal\Core\Asset\CssCollectionGrouper.
*/
public function testGrouper() {
$css_assets = [
'system.base.css' => [
'group' => -100,
'type' => 'file',
'weight' => 0.012,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'core/modules/system/system.base.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'system.base.css',
],
'js.module.css' => [
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'core/modules/system/js.module.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'js.module.css',
],
'jquery.ui.core.css' => [
'group' => -100,
'type' => 'file',
'weight' => 0.004,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'core/misc/ui/themes/base/jquery.ui.core.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'jquery.ui.core.css',
],
'field.css' => [
'group' => 0,
'type' => 'file',
'weight' => 0.011,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'core/modules/field/theme/field.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'field.css',
],
'external.css' => [
'group' => 0,
'type' => 'external',
'weight' => 0.009,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'http://example.com/external.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'external.css',
],
'elements.css' => [
'group' => 100,
'media' => 'all',
'type' => 'file',
'weight' => 0.001,
'preprocess' => TRUE,
'data' => 'core/themes/bartik/css/base/elements.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'elements.css',
],
'print.css' => [
'group' => 100,
'media' => 'print',
'type' => 'file',
'weight' => 0.003,
'preprocess' => TRUE,
'data' => 'core/themes/bartik/css/print.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'print.css',
],
];
$groups = $this->grouper->group($css_assets);
$this->assertSame(count($groups), 5, "5 groups created.");
// Check group 1.
$group = $groups[0];
$this->assertSame($group['group'], -100);
$this->assertSame($group['type'], 'file');
$this->assertSame($group['media'], 'all');
$this->assertSame($group['preprocess'], TRUE);
$this->assertSame(count($group['items']), 3);
$this->assertContains($css_assets['system.base.css'], $group['items']);
$this->assertContains($css_assets['js.module.css'], $group['items']);
// Check group 2.
$group = $groups[1];
$this->assertSame($group['group'], 0);
$this->assertSame($group['type'], 'file');
$this->assertSame($group['media'], 'all');
$this->assertSame($group['preprocess'], TRUE);
$this->assertSame(count($group['items']), 1);
$this->assertContains($css_assets['field.css'], $group['items']);
// Check group 3.
$group = $groups[2];
$this->assertSame($group['group'], 0);
$this->assertSame($group['type'], 'external');
$this->assertSame($group['media'], 'all');
$this->assertSame($group['preprocess'], TRUE);
$this->assertSame(count($group['items']), 1);
$this->assertContains($css_assets['external.css'], $group['items']);
// Check group 4.
$group = $groups[3];
$this->assertSame($group['group'], 100);
$this->assertSame($group['type'], 'file');
$this->assertSame($group['media'], 'all');
$this->assertSame($group['preprocess'], TRUE);
$this->assertSame(count($group['items']), 1);
$this->assertContains($css_assets['elements.css'], $group['items']);
// Check group 5.
$group = $groups[4];
$this->assertSame($group['group'], 100);
$this->assertSame($group['type'], 'file');
$this->assertSame($group['media'], 'print');
$this->assertSame($group['preprocess'], TRUE);
$this->assertSame(count($group['items']), 1);
$this->assertContains($css_assets['print.css'], $group['items']);
}
}
@@ -0,0 +1,519 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\CssCollectionRenderer;
use Drupal\Tests\UnitTestCase;
/**
* Tests the CSS asset collection renderer.
*
* @group Asset
*/
class CssCollectionRendererUnitTest extends UnitTestCase {
/**
* A CSS asset renderer.
*
* @var \Drupal\Core\Asset\CssRenderer object.
*/
protected $renderer;
/**
* A valid file CSS asset group.
*
* @var array
*/
protected $fileCssGroup;
/**
* The state mock class.
*
* @var \Drupal\Core\State\StateInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $state;
protected function setUp() {
parent::setUp();
$this->state = $this->getMock('Drupal\Core\State\StateInterface');
$this->renderer = new CssCollectionRenderer($this->state);
$this->fileCssGroup = [
'group' => -100,
'type' => 'file',
'media' => 'all',
'preprocess' => TRUE,
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'items' => [
0 => [
'group' => -100,
'type' => 'file',
'weight' => 0.012,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'tests/Drupal/Tests/Core/Asset/foo.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'foo.css',
],
1 => [
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'tests/Drupal/Tests/Core/Asset/bar.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'bar.css',
],
],
];
}
/**
* Provides data for the CSS asset rendering test.
*
* @see testRender
*/
public function providerTestRender() {
$create_link_element = function($href, $media = 'all', $browsers = []) {
return [
'#type' => 'html_tag',
'#tag' => 'link',
'#attributes' => [
'rel' => 'stylesheet',
'href' => $href,
'media' => $media,
],
'#browsers' => $browsers,
];
};
$create_style_element = function($value, $media, $browsers = []) {
$style_element = [
'#type' => 'html_tag',
'#tag' => 'style',
'#value' => $value,
'#attributes' => [
'media' => $media
],
'#browsers' => $browsers,
];
return $style_element;
};
$create_file_css_asset = function($data, $media = 'all', $preprocess = TRUE) {
return ['group' => 0, 'type' => 'file', 'media' => $media, 'preprocess' => $preprocess, 'data' => $data, 'browsers' => []];
};
return [
// Single external CSS asset.
0 => [
// CSS assets.
[
0 => ['group' => 0, 'type' => 'external', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'http://example.com/popular.js', 'browsers' => []],
],
// Render elements.
[
0 => $create_link_element('http://example.com/popular.js', 'all'),
],
],
// Single file CSS asset.
2 => [
[
0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all', 'browsers' => []],
],
[
0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/file-all')) . '?0', 'all'),
],
],
// 31 file CSS assets: expect 31 link elements.
3 => [
[
0 => $create_file_css_asset('public://css/1.css'),
1 => $create_file_css_asset('public://css/2.css'),
2 => $create_file_css_asset('public://css/3.css'),
3 => $create_file_css_asset('public://css/4.css'),
4 => $create_file_css_asset('public://css/5.css'),
5 => $create_file_css_asset('public://css/6.css'),
6 => $create_file_css_asset('public://css/7.css'),
7 => $create_file_css_asset('public://css/8.css'),
8 => $create_file_css_asset('public://css/9.css'),
9 => $create_file_css_asset('public://css/10.css'),
10 => $create_file_css_asset('public://css/11.css'),
11 => $create_file_css_asset('public://css/12.css'),
12 => $create_file_css_asset('public://css/13.css'),
13 => $create_file_css_asset('public://css/14.css'),
14 => $create_file_css_asset('public://css/15.css'),
15 => $create_file_css_asset('public://css/16.css'),
16 => $create_file_css_asset('public://css/17.css'),
17 => $create_file_css_asset('public://css/18.css'),
18 => $create_file_css_asset('public://css/19.css'),
19 => $create_file_css_asset('public://css/20.css'),
20 => $create_file_css_asset('public://css/21.css'),
21 => $create_file_css_asset('public://css/22.css'),
22 => $create_file_css_asset('public://css/23.css'),
23 => $create_file_css_asset('public://css/24.css'),
24 => $create_file_css_asset('public://css/25.css'),
25 => $create_file_css_asset('public://css/26.css'),
26 => $create_file_css_asset('public://css/27.css'),
27 => $create_file_css_asset('public://css/28.css'),
28 => $create_file_css_asset('public://css/29.css'),
29 => $create_file_css_asset('public://css/30.css'),
30 => $create_file_css_asset('public://css/31.css'),
],
[
0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/1.css')) . '?0'),
1 => $create_link_element(file_url_transform_relative(file_create_url('public://css/2.css')) . '?0'),
2 => $create_link_element(file_url_transform_relative(file_create_url('public://css/3.css')) . '?0'),
3 => $create_link_element(file_url_transform_relative(file_create_url('public://css/4.css')) . '?0'),
4 => $create_link_element(file_url_transform_relative(file_create_url('public://css/5.css')) . '?0'),
5 => $create_link_element(file_url_transform_relative(file_create_url('public://css/6.css')) . '?0'),
6 => $create_link_element(file_url_transform_relative(file_create_url('public://css/7.css')) . '?0'),
7 => $create_link_element(file_url_transform_relative(file_create_url('public://css/8.css')) . '?0'),
8 => $create_link_element(file_url_transform_relative(file_create_url('public://css/9.css')) . '?0'),
9 => $create_link_element(file_url_transform_relative(file_create_url('public://css/10.css')) . '?0'),
10 => $create_link_element(file_url_transform_relative(file_create_url('public://css/11.css')) . '?0'),
11 => $create_link_element(file_url_transform_relative(file_create_url('public://css/12.css')) . '?0'),
12 => $create_link_element(file_url_transform_relative(file_create_url('public://css/13.css')) . '?0'),
13 => $create_link_element(file_url_transform_relative(file_create_url('public://css/14.css')) . '?0'),
14 => $create_link_element(file_url_transform_relative(file_create_url('public://css/15.css')) . '?0'),
15 => $create_link_element(file_url_transform_relative(file_create_url('public://css/16.css')) . '?0'),
16 => $create_link_element(file_url_transform_relative(file_create_url('public://css/17.css')) . '?0'),
17 => $create_link_element(file_url_transform_relative(file_create_url('public://css/18.css')) . '?0'),
18 => $create_link_element(file_url_transform_relative(file_create_url('public://css/19.css')) . '?0'),
19 => $create_link_element(file_url_transform_relative(file_create_url('public://css/20.css')) . '?0'),
20 => $create_link_element(file_url_transform_relative(file_create_url('public://css/21.css')) . '?0'),
21 => $create_link_element(file_url_transform_relative(file_create_url('public://css/22.css')) . '?0'),
22 => $create_link_element(file_url_transform_relative(file_create_url('public://css/23.css')) . '?0'),
23 => $create_link_element(file_url_transform_relative(file_create_url('public://css/24.css')) . '?0'),
24 => $create_link_element(file_url_transform_relative(file_create_url('public://css/25.css')) . '?0'),
25 => $create_link_element(file_url_transform_relative(file_create_url('public://css/26.css')) . '?0'),
26 => $create_link_element(file_url_transform_relative(file_create_url('public://css/27.css')) . '?0'),
27 => $create_link_element(file_url_transform_relative(file_create_url('public://css/28.css')) . '?0'),
28 => $create_link_element(file_url_transform_relative(file_create_url('public://css/29.css')) . '?0'),
29 => $create_link_element(file_url_transform_relative(file_create_url('public://css/30.css')) . '?0'),
30 => $create_link_element(file_url_transform_relative(file_create_url('public://css/31.css')) . '?0'),
],
],
// 32 file CSS assets with the same properties: expect 2 style elements.
4 => [
[
0 => $create_file_css_asset('public://css/1.css'),
1 => $create_file_css_asset('public://css/2.css'),
2 => $create_file_css_asset('public://css/3.css'),
3 => $create_file_css_asset('public://css/4.css'),
4 => $create_file_css_asset('public://css/5.css'),
5 => $create_file_css_asset('public://css/6.css'),
6 => $create_file_css_asset('public://css/7.css'),
7 => $create_file_css_asset('public://css/8.css'),
8 => $create_file_css_asset('public://css/9.css'),
9 => $create_file_css_asset('public://css/10.css'),
10 => $create_file_css_asset('public://css/11.css'),
11 => $create_file_css_asset('public://css/12.css'),
12 => $create_file_css_asset('public://css/13.css'),
13 => $create_file_css_asset('public://css/14.css'),
14 => $create_file_css_asset('public://css/15.css'),
15 => $create_file_css_asset('public://css/16.css'),
16 => $create_file_css_asset('public://css/17.css'),
17 => $create_file_css_asset('public://css/18.css'),
18 => $create_file_css_asset('public://css/19.css'),
19 => $create_file_css_asset('public://css/20.css'),
20 => $create_file_css_asset('public://css/21.css'),
21 => $create_file_css_asset('public://css/22.css'),
22 => $create_file_css_asset('public://css/23.css'),
23 => $create_file_css_asset('public://css/24.css'),
24 => $create_file_css_asset('public://css/25.css'),
25 => $create_file_css_asset('public://css/26.css'),
26 => $create_file_css_asset('public://css/27.css'),
27 => $create_file_css_asset('public://css/28.css'),
28 => $create_file_css_asset('public://css/29.css'),
29 => $create_file_css_asset('public://css/30.css'),
30 => $create_file_css_asset('public://css/31.css'),
31 => $create_file_css_asset('public://css/32.css'),
],
[
0 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/1.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/2.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/3.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/4.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/5.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/6.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/7.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/8.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/9.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/10.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/11.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/12.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/13.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/14.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/15.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/16.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/17.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/18.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/19.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/20.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/21.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/22.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/23.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/24.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/25.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/26.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/27.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/28.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/29.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/30.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/31.css')) . '?0");
', 'all'),
1 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/32.css')) . '?0");
', 'all'),
],
],
// 32 file CSS assets with the same properties, except for the 10th and
// 20th files, they have different 'media' properties. Expect 5 style
// elements.
5 => [
[
0 => $create_file_css_asset('public://css/1.css'),
1 => $create_file_css_asset('public://css/2.css'),
2 => $create_file_css_asset('public://css/3.css'),
3 => $create_file_css_asset('public://css/4.css'),
4 => $create_file_css_asset('public://css/5.css'),
5 => $create_file_css_asset('public://css/6.css'),
6 => $create_file_css_asset('public://css/7.css'),
7 => $create_file_css_asset('public://css/8.css'),
8 => $create_file_css_asset('public://css/9.css'),
9 => $create_file_css_asset('public://css/10.css', 'screen'),
10 => $create_file_css_asset('public://css/11.css'),
11 => $create_file_css_asset('public://css/12.css'),
12 => $create_file_css_asset('public://css/13.css'),
13 => $create_file_css_asset('public://css/14.css'),
14 => $create_file_css_asset('public://css/15.css'),
15 => $create_file_css_asset('public://css/16.css'),
16 => $create_file_css_asset('public://css/17.css'),
17 => $create_file_css_asset('public://css/18.css'),
18 => $create_file_css_asset('public://css/19.css'),
19 => $create_file_css_asset('public://css/20.css', 'print'),
20 => $create_file_css_asset('public://css/21.css'),
21 => $create_file_css_asset('public://css/22.css'),
22 => $create_file_css_asset('public://css/23.css'),
23 => $create_file_css_asset('public://css/24.css'),
24 => $create_file_css_asset('public://css/25.css'),
25 => $create_file_css_asset('public://css/26.css'),
26 => $create_file_css_asset('public://css/27.css'),
27 => $create_file_css_asset('public://css/28.css'),
28 => $create_file_css_asset('public://css/29.css'),
29 => $create_file_css_asset('public://css/30.css'),
30 => $create_file_css_asset('public://css/31.css'),
31 => $create_file_css_asset('public://css/32.css'),
],
[
0 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/1.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/2.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/3.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/4.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/5.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/6.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/7.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/8.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/9.css')) . '?0");
', 'all'),
1 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/10.css')) . '?0");
', 'screen'),
2 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/11.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/12.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/13.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/14.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/15.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/16.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/17.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/18.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/19.css')) . '?0");
', 'all'),
3 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/20.css')) . '?0");
', 'print'),
4 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/21.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/22.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/23.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/24.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/25.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/26.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/27.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/28.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/29.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/30.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/31.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/32.css')) . '?0");
', 'all'),
],
],
// 32 file CSS assets with the same properties, except for the 15th, which
// has 'preprocess' = FALSE. Expect 1 link element and 2 style elements.
6 => [
[
0 => $create_file_css_asset('public://css/1.css'),
1 => $create_file_css_asset('public://css/2.css'),
2 => $create_file_css_asset('public://css/3.css'),
3 => $create_file_css_asset('public://css/4.css'),
4 => $create_file_css_asset('public://css/5.css'),
5 => $create_file_css_asset('public://css/6.css'),
6 => $create_file_css_asset('public://css/7.css'),
7 => $create_file_css_asset('public://css/8.css'),
8 => $create_file_css_asset('public://css/9.css'),
9 => $create_file_css_asset('public://css/10.css'),
10 => $create_file_css_asset('public://css/11.css'),
11 => $create_file_css_asset('public://css/12.css'),
12 => $create_file_css_asset('public://css/13.css'),
13 => $create_file_css_asset('public://css/14.css'),
14 => $create_file_css_asset('public://css/15.css', 'all', FALSE),
15 => $create_file_css_asset('public://css/16.css'),
16 => $create_file_css_asset('public://css/17.css'),
17 => $create_file_css_asset('public://css/18.css'),
18 => $create_file_css_asset('public://css/19.css'),
19 => $create_file_css_asset('public://css/20.css'),
20 => $create_file_css_asset('public://css/21.css'),
21 => $create_file_css_asset('public://css/22.css'),
22 => $create_file_css_asset('public://css/23.css'),
23 => $create_file_css_asset('public://css/24.css'),
24 => $create_file_css_asset('public://css/25.css'),
25 => $create_file_css_asset('public://css/26.css'),
26 => $create_file_css_asset('public://css/27.css'),
27 => $create_file_css_asset('public://css/28.css'),
28 => $create_file_css_asset('public://css/29.css'),
29 => $create_file_css_asset('public://css/30.css'),
30 => $create_file_css_asset('public://css/31.css'),
31 => $create_file_css_asset('public://css/32.css'),
],
[
0 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/1.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/2.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/3.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/4.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/5.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/6.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/7.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/8.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/9.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/10.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/11.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/12.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/13.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/14.css')) . '?0");
', 'all'),
1 => $create_link_element(file_url_transform_relative(file_create_url('public://css/15.css')) . '?0'),
2 => $create_style_element('
@import url("' . file_url_transform_relative(file_create_url('public://css/16.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/17.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/18.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/19.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/20.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/21.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/22.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/23.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/24.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/25.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/26.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/27.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/28.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/29.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/30.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/31.css')) . '?0");
@import url("' . file_url_transform_relative(file_create_url('public://css/32.css')) . '?0");
', 'all'),
],
],
];
}
/**
* Tests CSS asset rendering.
*
* @dataProvider providerTestRender
*/
public function testRender(array $css_assets, array $render_elements) {
$this->state->expects($this->once())
->method('get')
->with('system.css_js_query_string')
->will($this->returnValue(NULL));
$this->assertSame($render_elements, $this->renderer->render($css_assets));
}
/**
* Tests a CSS asset group with the invalid 'type' => 'internal'.
*/
public function testRenderInvalidType() {
$this->state->expects($this->once())
->method('get')
->with('system.css_js_query_string')
->will($this->returnValue(NULL));
$this->setExpectedException('Exception', 'Invalid CSS asset type.');
$css_group = [
'group' => 0,
'type' => 'internal',
'media' => 'all',
'preprocess' => TRUE,
'browsers' => [],
'data' => 'http://example.com/popular.js'
];
$this->renderer->render($css_group);
}
}
/**
* Temporary mock for file_create_url(), until that is moved into
* Component/Utility.
*/
if (!function_exists('Drupal\Tests\Core\Asset\file_create_url')) {
function file_create_url($uri) {
return 'file_create_url:' . $uri;
}
}
/**
* Temporary mock of file_url_transform_relative, until that is moved into
* Component/Utility.
*/
if (!function_exists('Drupal\Tests\Core\Asset\file_url_transform_relative')) {
function file_url_transform_relative($uri) {
return 'file_url_transform_relative:' . $uri;
}
}
/**
* CssCollectionRenderer uses file_create_url() & file_url_transform_relative(),
* which *are* available when using the Simpletest test runner, but not when
* using the PHPUnit test runner; hence this hack.
*/
namespace Drupal\Core\Asset;
if (!function_exists('Drupal\Core\Asset\file_create_url')) {
/**
* Temporary mock for file_create_url(), until that is moved into
* Component/Utility.
*/
function file_create_url($uri) {
return \Drupal\Tests\Core\Asset\file_create_url($uri);
}
}
if (!function_exists('Drupal\Core\Asset\file_url_transform_relative')) {
/**
* Temporary mock of file_url_transform_relative, until that is moved into
* Component/Utility.
*/
function file_url_transform_relative($uri) {
return \Drupal\Tests\Core\Asset\file_url_transform_relative($uri);
}
}
@@ -0,0 +1,322 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\CssOptimizer;
use Drupal\Tests\UnitTestCase;
/**
* Tests the CSS asset optimizer.
*
* @group Asset
*/
class CssOptimizerUnitTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected $backupGlobals = FALSE;
/**
* A CSS asset optimizer.
*
* @var \Drupal\Core\Asset\CssOptimizer object.
*/
protected $optimizer;
protected function setUp() {
parent::setUp();
$this->optimizer = new CssOptimizer();
}
/**
* Provides data for the CSS asset optimizing test.
*/
public function providerTestOptimize() {
$path = 'core/tests/Drupal/Tests/Core/Asset/css_test_files/';
$absolute_path = dirname(__FILE__) . '/css_test_files/';
return [
// File. Tests:
// - Stripped comments and white-space.
// - Retain white-space in selectors. (https://www.drupal.org/node/472820)
// - Retain pseudo-selectors. (https://www.drupal.org/node/460448)
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.012,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_input_without_import.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_without_import.css',
],
file_get_contents($absolute_path . 'css_input_without_import.css.optimized.css'),
],
// File. Tests:
// - Proper URLs in imported files. (https://www.drupal.org/node/265719)
// - A background image with relative paths, which must be rewritten.
// - The rewritten background image path must also be passed through
// file_create_url(). (https://www.drupal.org/node/1961340)
// - Imported files that are external (protocol-relative URL or not)
// should not be expanded. (https://www.drupal.org/node/2014851)
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_input_with_import.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_with_import.css',
],
str_replace('url(images/icon.png)', 'url(' . file_url_transform_relative(file_create_url($path . 'images/icon.png')) . ')', file_get_contents($absolute_path . 'css_input_with_import.css.optimized.css')),
],
// File. Tests:
// - Retain comment hacks.
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'comment_hacks.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'comment_hacks.css',
],
file_get_contents($absolute_path . 'comment_hacks.css.optimized.css'),
],
// File in subfolder. Tests:
// - CSS import path is properly interpreted.
// (https://www.drupal.org/node/1198904)
// - Don't adjust data URIs (https://www.drupal.org/node/2142441)
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_subfolder/css_input_with_import.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_with_import.css',
],
str_replace('url(../images/icon.png)', 'url(' . file_url_transform_relative(file_create_url($path . 'images/icon.png')) . ')', file_get_contents($absolute_path . 'css_subfolder/css_input_with_import.css.optimized.css')),
],
// File. Tests:
// - Any @charaset declaration at the beginning of a file should be
// removed without breaking subsequent CSS.
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'charset_sameline.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'charset_sameline.css',
],
file_get_contents($absolute_path . 'charset.css.optimized.css'),
],
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'charset_newline.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'charset_newline.css',
],
file_get_contents($absolute_path . 'charset.css.optimized.css'),
],
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_input_with_bom.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_with_bom.css',
],
'.byte-order-mark-test{content:"☃";}' . "\n",
],
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_input_with_charset.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_with_charset.css',
],
'.charset-test{content:"€";}' . "\n",
],
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_input_with_bom_and_charset.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_with_bom_and_charset.css',
],
'.byte-order-mark-charset-test{content:"☃";}' . "\n",
],
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'css_input_with_utf16_bom.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'css_input_with_utf16_bom.css',
],
'.utf16-byte-order-mark-test{content:"☃";}' . "\n",
],
[
[
'group' => -100,
'type' => 'file',
'weight' => 0.013,
'media' => 'all',
'preprocess' => TRUE,
'data' => $path . 'quotes.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'quotes.css',
],
file_get_contents($absolute_path . 'quotes.css.optimized.css'),
],
];
}
/**
* Tests optimizing a CSS asset group containing 'type' => 'file'.
*
* @dataProvider providerTestOptimize
*/
public function testOptimize($css_asset, $expected) {
global $base_path;
$original_base_path = $base_path;
$base_path = '/';
// \Drupal\Core\Asset\CssOptimizer::loadFile() relies on the current working
// directory being the one that is used when index.php is the entry point.
// Note: PHPUnit automatically restores the original working directory.
chdir(realpath(__DIR__ . '/../../../../../../'));
$this->assertEquals($expected, $this->optimizer->optimize($css_asset), 'Group of file CSS assets optimized correctly.');
$base_path = $original_base_path;
}
/**
* Tests a file CSS asset with preprocessing disabled.
*/
public function testTypeFilePreprocessingDisabled() {
$this->setExpectedException('Exception', 'Only file CSS assets with preprocessing enabled can be optimized.');
$css_asset = [
'group' => -100,
'type' => 'file',
'weight' => 0.012,
'media' => 'all',
// Preprocessing disabled.
'preprocess' => FALSE,
'data' => 'tests/Drupal/Tests/Core/Asset/foo.css',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
'basename' => 'foo.css',
];
$this->optimizer->optimize($css_asset);
}
/**
* Tests a CSS asset with 'type' => 'external'.
*/
public function testTypeExternal() {
$this->setExpectedException('Exception', 'Only file CSS assets can be optimized.');
$css_asset = [
'group' => -100,
// Type external.
'type' => 'external',
'weight' => 0.012,
'media' => 'all',
'preprocess' => TRUE,
'data' => 'http://example.com/foo.js',
'browsers' => ['IE' => TRUE, '!IE' => TRUE],
];
$this->optimizer->optimize($css_asset);
}
}
/**
* Temporary mock for file_create_url(), until that is moved into
* Component/Utility.
*/
if (!function_exists('Drupal\Tests\Core\Asset\file_create_url')) {
function file_create_url($uri) {
return 'file_create_url:' . $uri;
}
}
/**
* Temporary mock of file_url_transform_relative, until that is moved into
* Component/Utility.
*/
if (!function_exists('Drupal\Tests\Core\Asset\file_url_transform_relative')) {
function file_url_transform_relative($uri) {
return 'file_url_transform_relative:' . $uri;
}
}
/**
* CssCollectionRenderer uses file_create_url() & file_url_transform_relative(),
* which *are* available when using the Simpletest test runner, but not when
* using the PHPUnit test runner; hence this hack.
*/
namespace Drupal\Core\Asset;
if (!function_exists('Drupal\Core\Asset\file_create_url')) {
/**
* Temporary mock for file_create_url(), until that is moved into
* Component/Utility.
*/
function file_create_url($uri) {
return \Drupal\Tests\Core\Asset\file_create_url($uri);
}
}
if (!function_exists('Drupal\Core\Asset\file_url_transform_relative')) {
/**
* Temporary mock of file_url_transform_relative, until that is moved into
* Component/Utility.
*/
function file_url_transform_relative($uri) {
return \Drupal\Tests\Core\Asset\file_url_transform_relative($uri);
}
}
if (!function_exists('Drupal\Core\Asset\file_uri_scheme')) {
function file_uri_scheme($uri) {
return FALSE;
}
}
@@ -0,0 +1,126 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\JsOptimizer;
use Drupal\Tests\UnitTestCase;
/**
* Tests the JS asset optimizer.
*
* @group Asset
*/
class JsOptimizerUnitTest extends UnitTestCase {
/**
* A JS asset optimizer.
*
* @var \Drupal\Core\Asset\JsOptimizer object.
*/
protected $optimizer;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->optimizer = new JsOptimizer();
}
/**
* Provides data for the JS asset cleaning test.
*
* @see \Drupal\Core\Asset\JsOptimizer::clean()
*
* @returns array
* An array of test data.
*/
public function providerTestClean() {
$path = dirname(__FILE__) . '/js_test_files/';
return [
// File. Tests:
// - Stripped sourceMappingURL with comment # syntax.
0 => [
file_get_contents($path . 'source_mapping_url.min.js'),
file_get_contents($path . 'source_mapping_url.min.js.optimized.js'),
],
// File. Tests:
// - Stripped sourceMappingURL with comment @ syntax.
1 => [
file_get_contents($path . 'source_mapping_url_old.min.js'),
file_get_contents($path . 'source_mapping_url_old.min.js.optimized.js'),
],
// File. Tests:
// - Stripped sourceURL with comment # syntax.
2 => [
file_get_contents($path . 'source_url.min.js'),
file_get_contents($path . 'source_url.min.js.optimized.js'),
],
// File. Tests:
// - Stripped sourceURL with comment @ syntax.
3 => [
file_get_contents($path . 'source_url_old.min.js'),
file_get_contents($path . 'source_url_old.min.js.optimized.js'),
],
];
}
/**
* Tests cleaning of a JS asset group containing 'type' => 'file'.
*
* @dataProvider providerTestClean
*/
public function testClean($js_asset, $expected) {
$this->assertEquals($expected, $this->optimizer->clean($js_asset));
}
/**
* Provides data for the JS asset optimize test.
*
* @see \Drupal\Core\Asset\JsOptimizer::optimize()
*
* @returns array
* An array of test data.
*/
public function providerTestOptimize() {
$path = dirname(__FILE__) . '/js_test_files/';
return [
0 => [
[
'type' => 'file',
'preprocess' => TRUE,
'data' => $path . 'utf8_bom.js',
],
file_get_contents($path . 'utf8_bom.js.optimized.js'),
],
1 => [
[
'type' => 'file',
'preprocess' => TRUE,
'data' => $path . 'utf16_bom.js',
],
file_get_contents($path . 'utf16_bom.js.optimized.js'),
],
2 => [
[
'type' => 'file',
'preprocess' => TRUE,
'data' => $path . 'latin_9.js',
'attributes' => ['charset' => 'ISO-8859-15'],
],
file_get_contents($path . 'latin_9.js.optimized.js'),
],
];
}
/**
* Tests cleaning of a JS asset group containing 'type' => 'file'.
*
* @dataProvider providerTestOptimize
*/
public function testOptimize($js_asset, $expected) {
$this->assertEquals($expected, $this->optimizer->optimize($js_asset));
}
}
@@ -0,0 +1,172 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\LibraryDependencyResolver;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Asset\LibraryDependencyResolver
* @group Asset
*/
class LibraryDependencyResolverTest extends UnitTestCase {
/**
* The tested library dependency resolver.
*
* @var \Drupal\Core\Asset\LibraryDependencyResolver
*/
protected $libraryDependencyResolver;
/**
* The mocked library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $libraryDiscovery;
/**
* The mocked module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $moduleHandler;
/**
* Test library data.
*
* @var array
*/
protected $libraryData = [
'no_deps_a' => ['js' => [], 'css' => []],
'no_deps_b' => ['js' => [], 'css' => []],
'no_deps_c' => ['js' => [], 'css' => []],
'deps_a' => ['js' => [], 'css' => [], 'dependencies' => ['test/no_deps_a']],
'deps_b' => ['js' => [], 'css' => [], 'dependencies' => ['test/no_deps_a', 'test/no_deps_b']],
'deps_c' => ['js' => [], 'css' => [], 'dependencies' => ['test/no_deps_b', 'test/no_deps_a']],
'nested_deps_a' => ['js' => [], 'css' => [], 'dependencies' => ['test/deps_a']],
'nested_deps_b' => ['js' => [], 'css' => [], 'dependencies' => ['test/nested_deps_a']],
'nested_deps_c' => ['js' => [], 'css' => [], 'dependencies' => ['test/nested_deps_b']],
];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->libraryDiscovery = $this->getMockBuilder('Drupal\Core\Asset\LibraryDiscovery')
->disableOriginalConstructor()
->setMethods(['getLibrariesByExtension'])
->getMock();
$this->libraryDiscovery->expects($this->any())
->method('getLibrariesByExtension')
->with('test')
->will($this->returnValue($this->libraryData));
$this->libraryDependencyResolver = new LibraryDependencyResolver($this->libraryDiscovery);
}
/**
* Provides test data for ::testGetLibrariesWithDependencies().
*/
public function providerTestGetLibrariesWithDependencies() {
return [
// Empty list of libraries.
[[], []],
// Without dependencies.
[['test/no_deps_a'], ['test/no_deps_a']],
[['test/no_deps_a', 'test/no_deps_b'], ['test/no_deps_a', 'test/no_deps_b']],
[['test/no_deps_b', 'test/no_deps_a'], ['test/no_deps_b', 'test/no_deps_a']],
// Single-level (direct) dependencies.
[['test/deps_a'], ['test/no_deps_a', 'test/deps_a']],
[['test/deps_b'], ['test/no_deps_a', 'test/no_deps_b', 'test/deps_b']],
[['test/deps_c'], ['test/no_deps_b', 'test/no_deps_a', 'test/deps_c']],
[['test/deps_a', 'test/deps_b'], ['test/no_deps_a', 'test/deps_a', 'test/no_deps_b', 'test/deps_b']],
[['test/deps_a', 'test/deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/no_deps_b', 'test/deps_c']],
[['test/deps_a', 'test/deps_b', 'test/deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/no_deps_b', 'test/deps_b', 'test/deps_c']],
[['test/deps_b', 'test/deps_a'], ['test/no_deps_a', 'test/no_deps_b', 'test/deps_b', 'test/deps_a']],
[['test/deps_b', 'test/deps_c'], ['test/no_deps_a', 'test/no_deps_b', 'test/deps_b', 'test/deps_c']],
[['test/deps_c', 'test/deps_b'], ['test/no_deps_b', 'test/no_deps_a', 'test/deps_c', 'test/deps_b']],
// Multi-level (indirect) dependencies.
[['test/nested_deps_a'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a']],
[['test/nested_deps_b'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b']],
[['test/nested_deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_a', 'test/nested_deps_b'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b']],
[['test/nested_deps_b', 'test/nested_deps_a'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b']],
[['test/nested_deps_a', 'test/nested_deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_b', 'test/nested_deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_c', 'test/nested_deps_a'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_a', 'test/nested_deps_c', 'test/nested_deps_b'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_b', 'test/nested_deps_a', 'test/nested_deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_b', 'test/nested_deps_c', 'test/nested_deps_a'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_c', 'test/nested_deps_a', 'test/nested_deps_b'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
[['test/nested_deps_c', 'test/nested_deps_b', 'test/nested_deps_a'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c']],
// Complex dependencies, combining the above, with many intersections.
[['test/deps_c', 'test/nested_deps_b'], ['test/no_deps_b', 'test/no_deps_a', 'test/deps_c', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b']],
[['test/no_deps_a', 'test/deps_c', 'test/nested_deps_b'], ['test/no_deps_a', 'test/no_deps_b', 'test/deps_c', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b']],
[['test/nested_deps_b', 'test/deps_c', 'test/no_deps_c'], ['test/no_deps_a', 'test/deps_a', 'test/nested_deps_a', 'test/nested_deps_b', 'test/no_deps_b', 'test/deps_c', 'test/no_deps_c']],
];
}
/**
* @covers ::getLibrariesWithDependencies
*
* @dataProvider providerTestGetLibrariesWithDependencies
*/
public function testGetLibrariesWithDependencies(array $libraries, array $expected) {
$this->assertEquals($expected, $this->libraryDependencyResolver->getLibrariesWithDependencies($libraries));
}
/**
* Provides test data for ::testGetMinimalRepresentativeSubset().
*/
public function providerTestGetMinimalRepresentativeSubset() {
return [
// Empty list of libraries.
[[], []],
// Without dependencies.
[['test/no_deps_a'], ['test/no_deps_a']],
[['test/no_deps_a', 'test/no_deps_b'], ['test/no_deps_a', 'test/no_deps_b']],
[['test/no_deps_b', 'test/no_deps_a'], ['test/no_deps_b', 'test/no_deps_a']],
// Single-level (direct) dependencies.
[['test/deps_a'], ['test/deps_a']],
[['test/deps_b'], ['test/deps_b']],
[['test/deps_c'], ['test/deps_c']],
[['test/deps_a', 'test/deps_b'], ['test/deps_a', 'test/deps_b']],
[['test/deps_a', 'test/deps_c'], ['test/deps_a', 'test/deps_c']],
[['test/deps_a', 'test/deps_b', 'test/deps_c'], ['test/deps_a', 'test/deps_b', 'test/deps_c']],
[['test/deps_b', 'test/deps_a'], ['test/deps_b', 'test/deps_a']],
[['test/deps_b', 'test/deps_c'], ['test/deps_b', 'test/deps_c']],
[['test/deps_c', 'test/deps_b'], ['test/deps_c', 'test/deps_b']],
// Multi-level (indirect) dependencies.
[['test/nested_deps_a'], ['test/nested_deps_a']],
[['test/nested_deps_b'], ['test/nested_deps_b']],
[['test/nested_deps_c'], ['test/nested_deps_c']],
[['test/nested_deps_a', 'test/nested_deps_b'], ['test/nested_deps_b']],
[['test/nested_deps_b', 'test/nested_deps_a'], ['test/nested_deps_b']],
[['test/nested_deps_a', 'test/nested_deps_c'], ['test/nested_deps_c']],
[['test/nested_deps_b', 'test/nested_deps_c'], ['test/nested_deps_c']],
[['test/nested_deps_c', 'test/nested_deps_a'], ['test/nested_deps_c']],
[['test/nested_deps_a', 'test/nested_deps_b', 'test/nested_deps_c'], ['test/nested_deps_c']],
[['test/nested_deps_a', 'test/nested_deps_c', 'test/nested_deps_b'], ['test/nested_deps_c']],
[['test/nested_deps_b', 'test/nested_deps_a', 'test/nested_deps_c'], ['test/nested_deps_c']],
[['test/nested_deps_b', 'test/nested_deps_c', 'test/nested_deps_a'], ['test/nested_deps_c']],
[['test/nested_deps_c', 'test/nested_deps_a', 'test/nested_deps_b'], ['test/nested_deps_c']],
[['test/nested_deps_c', 'test/nested_deps_b', 'test/nested_deps_a'], ['test/nested_deps_c']],
// Complex dependencies, combining the above, with many intersections.
[['test/deps_c', 'test/nested_deps_b'], ['test/deps_c', 'test/nested_deps_b']],
[['test/no_deps_a', 'test/deps_c', 'test/nested_deps_b'], ['test/deps_c', 'test/nested_deps_b']],
[['test/nested_deps_b', 'test/deps_c', 'test/no_deps_c'], ['test/nested_deps_b', 'test/deps_c', 'test/no_deps_c']],
];
}
/**
* @covers ::getMinimalRepresentativeSubset
*
* @dataProvider providerTestGetMinimalRepresentativeSubset
*/
public function testGetMinimalRepresentativeSubset(array $libraries, array $expected) {
$this->assertEquals($expected, $this->libraryDependencyResolver->getMinimalRepresentativeSubset($libraries));
}
}
@@ -0,0 +1,153 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\LibraryDiscoveryCollector;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Asset\LibraryDiscoveryCollector
* @group Asset
*/
class LibraryDiscoveryCollectorTest extends UnitTestCase {
/**
* The mock cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;
/**
* The mock lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $lock;
/**
* The mock library discovery parser.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryParser|\PHPUnit_Framework_MockObject_MockObject
*/
protected $libraryDiscoveryParser;
/**
* The library discovery collector under test.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryCollector
*/
protected $libraryDiscoveryCollector;
/**
* The mocked theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $themeManager;
/**
* Test library data.
*
* @var array
*/
protected $libraryData = [
'test_1' => [
'js' => [],
'css' => [],
],
'test_2' => [
'js' => [],
'css' => [],
],
];
protected $activeTheme;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$this->themeManager = $this->getMockBuilder('Drupal\Core\Theme\ThemeManagerInterface')
->disableOriginalConstructor()
->getMock();
$this->libraryDiscoveryParser = $this->getMockBuilder('Drupal\Core\Asset\LibraryDiscoveryParser')
->disableOriginalConstructor()
->getMock();
}
/**
* Tests the resolve cache miss function.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMiss() {
$this->activeTheme = $this->getMockBuilder('Drupal\Core\Theme\ActiveTheme')
->disableOriginalConstructor()
->getMock();
$this->themeManager->expects($this->exactly(3))
->method('getActiveTheme')
->willReturn($this->activeTheme);
$this->activeTheme->expects($this->once())
->method('getName')
->willReturn('kitten_theme');
$this->libraryDiscoveryCollector = new LibraryDiscoveryCollector($this->cache, $this->lock, $this->libraryDiscoveryParser, $this->themeManager);
$this->libraryDiscoveryParser->expects($this->once())
->method('buildByExtension')
->with('test')
->willReturn($this->libraryData);
$this->assertSame($this->libraryData, $this->libraryDiscoveryCollector->get('test'));
$this->assertSame($this->libraryData, $this->libraryDiscoveryCollector->get('test'));
}
/**
* Tests the destruct method.
*
* @covers ::destruct
*/
public function testDestruct() {
$this->activeTheme = $this->getMockBuilder('Drupal\Core\Theme\ActiveTheme')
->disableOriginalConstructor()
->getMock();
$this->themeManager->expects($this->exactly(3))
->method('getActiveTheme')
->willReturn($this->activeTheme);
$this->activeTheme->expects($this->once())
->method('getName')
->willReturn('kitten_theme');
$this->libraryDiscoveryCollector = new LibraryDiscoveryCollector($this->cache, $this->lock, $this->libraryDiscoveryParser, $this->themeManager);
$this->libraryDiscoveryParser->expects($this->once())
->method('buildByExtension')
->with('test')
->willReturn($this->libraryData);
$lock_key = 'library_info:kitten_theme:Drupal\Core\Cache\CacheCollector';
$this->lock->expects($this->once())
->method('acquire')
->with($lock_key)
->will($this->returnValue(TRUE));
$this->cache->expects($this->exactly(2))
->method('get')
->with('library_info:kitten_theme')
->willReturn(FALSE);
$this->cache->expects($this->once())
->method('set')
->with('library_info:kitten_theme', ['test' => $this->libraryData], Cache::PERMANENT, ['library_info']);
$this->lock->expects($this->once())
->method('release')
->with($lock_key);
// This should get data and persist the key.
$this->libraryDiscoveryCollector->get('test');
$this->libraryDiscoveryCollector->destruct();
}
}
@@ -0,0 +1,566 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Asset\LibraryDiscoveryParserTest.
*/
namespace Drupal\Tests\Core\Asset;
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
use Drupal\Core\Asset\LibraryDiscoveryParser;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Asset\LibraryDiscoveryParser
* @group Asset
*/
class LibraryDiscoveryParserTest extends UnitTestCase {
/**
* The tested library discovery parser service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryParser|\Drupal\Tests\Core\Asset\TestLibraryDiscoveryParser
*/
protected $libraryDiscoveryParser;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;
/**
* The mocked module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $moduleHandler;
/**
* The mocked theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $themeManager;
/**
* The mocked lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $lock;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
$mock_active_theme = $this->getMockBuilder('Drupal\Core\Theme\ActiveTheme')
->disableOriginalConstructor()
->getMock();
$mock_active_theme->expects($this->any())
->method('getLibrariesOverride')
->willReturn([]);
$this->themeManager->expects($this->any())
->method('getActiveTheme')
->willReturn($mock_active_theme);
$this->libraryDiscoveryParser = new TestLibraryDiscoveryParser($this->root, $this->moduleHandler, $this->themeManager);
}
/**
* Tests that basic functionality works for getLibraryByName.
*
* @covers ::buildByExtension
*/
public function testBuildByExtensionSimple() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('example_module')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'example_module', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('example_module');
$library = $libraries['example'];
$this->assertCount(0, $library['js']);
$this->assertCount(1, $library['css']);
$this->assertCount(0, $library['dependencies']);
$this->assertEquals($path . '/css/example.css', $library['css'][0]['data']);
// Ensures that VERSION is replaced by the current core version.
$this->assertEquals(\Drupal::VERSION, $library['version']);
}
/**
* Tests that a theme can be used instead of a module.
*
* @covers ::buildByExtension
*/
public function testBuildByExtensionWithTheme() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('example_theme')
->will($this->returnValue(FALSE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('theme', 'example_theme', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('example_theme');
$library = $libraries['example'];
$this->assertCount(0, $library['js']);
$this->assertCount(1, $library['css']);
$this->assertCount(0, $library['dependencies']);
$this->assertEquals($path . '/css/example.css', $library['css'][0]['data']);
}
/**
* Tests that a module with a missing library file results in FALSE.
*
* @covers ::buildByExtension
*/
public function testBuildByExtensionWithMissingLibraryFile() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('example_module')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files_not_existing';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'example_module', $path);
$this->assertSame($this->libraryDiscoveryParser->buildByExtension('example_module'), []);
}
/**
* Tests that an exception is thrown when a libraries file couldn't be parsed.
*
* @covers ::buildByExtension
*/
public function testInvalidLibrariesFile() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('invalid_file')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'invalid_file', $path);
$this->setExpectedException(InvalidLibraryFileException::class);
$this->libraryDiscoveryParser->buildByExtension('invalid_file');
}
/**
* Tests that an exception is thrown when no CSS/JS/setting is specified.
*
* @covers ::buildByExtension
*/
public function testBuildByExtensionWithMissingInformation() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('example_module_missing_information')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'example_module_missing_information', $path);
$this->setExpectedException(IncompleteLibraryDefinitionException::class, "Incomplete library definition for definition 'example' in extension 'example_module_missing_information'");
$this->libraryDiscoveryParser->buildByExtension('example_module_missing_information');
}
/**
* Tests the version property, and how it propagates to the contained assets.
*
* @covers ::buildByExtension
*/
public function testVersion() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('versions')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'versions', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('versions');
$this->assertFalse(array_key_exists('version', $libraries['versionless']));
$this->assertEquals(-1, $libraries['versionless']['css'][0]['version']);
$this->assertEquals(-1, $libraries['versionless']['js'][0]['version']);
$this->assertEquals('9.8.7.6', $libraries['versioned']['version']);
$this->assertEquals('9.8.7.6', $libraries['versioned']['css'][0]['version']);
$this->assertEquals('9.8.7.6', $libraries['versioned']['js'][0]['version']);
$this->assertEquals(\Drupal::VERSION, $libraries['core-versioned']['version']);
$this->assertEquals(\Drupal::VERSION, $libraries['core-versioned']['css'][0]['version']);
$this->assertEquals(\Drupal::VERSION, $libraries['core-versioned']['js'][0]['version']);
}
/**
* Tests that the version property of external libraries is handled.
*
* @covers ::buildByExtension
*/
public function testExternalLibraries() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('external')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'external', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('external');
$library = $libraries['example_external'];
$this->assertEquals('http://example.com/css/example_external.css', $library['css'][0]['data']);
$this->assertEquals('http://example.com/example_external.js', $library['js'][0]['data']);
$this->assertEquals('3.14', $library['version']);
}
/**
* Ensures that CSS weights are taken into account properly.
*
* @covers ::buildByExtension
*/
public function testDefaultCssWeights() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('css_weights')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'css_weights', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('css_weights');
$library = $libraries['example'];
$css = $library['css'];
$this->assertCount(10, $css);
// The following default weights are tested:
// - CSS_BASE: -200
// - CSS_LAYOUT: -100
// - CSS_COMPONENT: 0
// - CSS_STATE: 100
// - CSS_THEME: 200
$this->assertEquals(200, $css[0]['weight']);
$this->assertEquals(200 + 29, $css[1]['weight']);
$this->assertEquals(-200, $css[2]['weight']);
$this->assertEquals(-200 + 97, $css[3]['weight']);
$this->assertEquals(-100, $css[4]['weight']);
$this->assertEquals(-100 + 92, $css[5]['weight']);
$this->assertEquals(0, $css[6]['weight']);
$this->assertEquals(45, $css[7]['weight']);
$this->assertEquals(100, $css[8]['weight']);
$this->assertEquals(100 + 8, $css[9]['weight']);
}
/**
* Ensures that you cannot provide positive weights for JavaScript libraries.
*
* @covers ::buildByExtension
*/
public function testJsWithPositiveWeight() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('js_positive_weight')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'js_positive_weight', $path);
$this->setExpectedException(\UnexpectedValueException::class);
$this->libraryDiscoveryParser->buildByExtension('js_positive_weight');
}
/**
* Tests a library with CSS/JavaScript and a setting.
*
* @covers ::buildByExtension
*/
public function testLibraryWithCssJsSetting() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('css_js_settings')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'css_js_settings', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('css_js_settings');
$library = $libraries['example'];
// Ensures that the group and type are set automatically.
$this->assertEquals(-100, $library['js'][0]['group']);
$this->assertEquals('file', $library['js'][0]['type']);
$this->assertEquals($path . '/js/example.js', $library['js'][0]['data']);
$this->assertEquals(0, $library['css'][0]['group']);
$this->assertEquals('file', $library['css'][0]['type']);
$this->assertEquals($path . '/css/base.css', $library['css'][0]['data']);
$this->assertEquals(['key' => 'value'], $library['drupalSettings']);
}
/**
* Tests a library with dependencies.
*
* @covers ::buildByExtension
*/
public function testLibraryWithDependencies() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('dependencies')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'dependencies', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('dependencies');
$library = $libraries['example'];
$this->assertCount(2, $library['dependencies']);
$this->assertEquals('external/example_external', $library['dependencies'][0]);
$this->assertEquals('example_module/example', $library['dependencies'][1]);
}
/**
* Tests a library with a couple of data formats like full URL.
*
* @covers ::buildByExtension
*/
public function testLibraryWithDataTypes() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('data_types')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'data_types', $path);
$this->libraryDiscoveryParser->setFileValidUri('public://test.css', TRUE);
$this->libraryDiscoveryParser->setFileValidUri('public://test2.css', FALSE);
$libraries = $this->libraryDiscoveryParser->buildByExtension('data_types');
$library = $libraries['example'];
$this->assertCount(5, $library['css']);
$this->assertEquals('external', $library['css'][0]['type']);
$this->assertEquals('http://example.com/test.css', $library['css'][0]['data']);
$this->assertEquals('file', $library['css'][1]['type']);
$this->assertEquals('tmp/test.css', $library['css'][1]['data']);
$this->assertEquals('external', $library['css'][2]['type']);
$this->assertEquals('//cdn.com/test.css', $library['css'][2]['data']);
$this->assertEquals('file', $library['css'][3]['type']);
$this->assertEquals('public://test.css', $library['css'][3]['data']);
}
/**
* Tests a library with JavaScript-specific flags.
*
* @covers ::buildByExtension
*/
public function testLibraryWithJavaScript() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('js')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'js', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('js');
$library = $libraries['example'];
$this->assertCount(2, $library['js']);
$this->assertEquals(FALSE, $library['js'][0]['minified']);
$this->assertEquals(TRUE, $library['js'][1]['minified']);
}
/**
* Tests that an exception is thrown when license is missing when 3rd party.
*
* @covers ::buildByExtension
*/
public function testLibraryThirdPartyWithMissingLicense() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('licenses_missing_information')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'licenses_missing_information', $path);
$this->setExpectedException(LibraryDefinitionMissingLicenseException::class, "Missing license information in library definition for definition 'no-license-info-but-remote' extension 'licenses_missing_information': it has a remote, but no license.");
$this->libraryDiscoveryParser->buildByExtension('licenses_missing_information');
}
/**
* Tests a library with various licenses, some GPL-compatible, some not.
*
* @covers ::buildByExtension
*/
public function testLibraryWithLicenses() {
$this->moduleHandler->expects($this->atLeastOnce())
->method('moduleExists')
->with('licenses')
->will($this->returnValue(TRUE));
$path = __DIR__ . '/library_test_files';
$path = substr($path, strlen($this->root) + 1);
$this->libraryDiscoveryParser->setPaths('module', 'licenses', $path);
$libraries = $this->libraryDiscoveryParser->buildByExtension('licenses');
// For libraries without license info, the default license is applied.
$library = $libraries['no-license-info'];
$this->assertCount(1, $library['css']);
$this->assertCount(1, $library['js']);
$this->assertTrue(isset($library['license']));
$default_license = [
'name' => 'GNU-GPL-2.0-or-later',
'url' => 'https://www.drupal.org/licensing/faq',
'gpl-compatible' => TRUE,
];
$this->assertEquals($library['license'], $default_license);
// GPL2-licensed libraries.
$library = $libraries['gpl2'];
$this->assertCount(1, $library['css']);
$this->assertCount(1, $library['js']);
$expected_license = [
'name' => 'gpl2',
'url' => 'https://url-to-gpl2-license',
'gpl-compatible' => TRUE,
];
$this->assertEquals($library['license'], $expected_license);
// MIT-licensed libraries.
$library = $libraries['mit'];
$this->assertCount(1, $library['css']);
$this->assertCount(1, $library['js']);
$expected_license = [
'name' => 'MIT',
'url' => 'https://url-to-mit-license',
'gpl-compatible' => TRUE,
];
$this->assertEquals($library['license'], $expected_license);
// Libraries in the Public Domain.
$library = $libraries['public-domain'];
$this->assertCount(1, $library['css']);
$this->assertCount(1, $library['js']);
$expected_license = [
'name' => 'Public Domain',
'url' => 'https://url-to-public-domain-license',
'gpl-compatible' => TRUE,
];
$this->assertEquals($library['license'], $expected_license);
// Apache-licensed libraries.
$library = $libraries['apache'];
$this->assertCount(1, $library['css']);
$this->assertCount(1, $library['js']);
$expected_license = [
'name' => 'apache',
'url' => 'https://url-to-apache-license',
'gpl-compatible' => FALSE,
];
$this->assertEquals($library['license'], $expected_license);
// Copyrighted libraries.
$library = $libraries['copyright'];
$this->assertCount(1, $library['css']);
$this->assertCount(1, $library['js']);
$expected_license = [
'name' => '© Some company',
'gpl-compatible' => FALSE,
];
$this->assertEquals($library['license'], $expected_license);
}
}
/**
* Wraps the tested class to mock the external dependencies.
*/
class TestLibraryDiscoveryParser extends LibraryDiscoveryParser {
protected $paths;
protected $validUris;
protected function drupalGetPath($type, $name) {
return isset($this->paths[$type][$name]) ? $this->paths[$type][$name] : NULL;
}
public function setPaths($type, $name, $path) {
$this->paths[$type][$name] = $path;
}
protected function fileValidUri($source) {
return isset($this->validUris[$source]) ? $this->validUris[$source] : FALSE;
}
public function setFileValidUri($source, $valid) {
$this->validUris[$source] = $valid;
}
}
if (!defined('CSS_AGGREGATE_DEFAULT')) {
define('CSS_AGGREGATE_DEFAULT', 0);
}
if (!defined('CSS_AGGREGATE_THEME')) {
define('CSS_AGGREGATE_THEME', 100);
}
if (!defined('CSS_BASE')) {
define('CSS_BASE', -200);
}
if (!defined('CSS_LAYOUT')) {
define('CSS_LAYOUT', -100);
}
if (!defined('CSS_COMPONENT')) {
define('CSS_COMPONENT', 0);
}
if (!defined('CSS_STATE')) {
define('CSS_STATE', 100);
}
if (!defined('CSS_THEME')) {
define('CSS_THEME', 200);
}
if (!defined('JS_SETTING')) {
define('JS_SETTING', -200);
}
if (!defined('JS_LIBRARY')) {
define('JS_LIBRARY', -100);
}
if (!defined('JS_DEFAULT')) {
define('JS_DEFAULT', 0);
}
if (!defined('JS_THEME')) {
define('JS_THEME', 100);
}

Some files were not shown because too many files have changed in this diff Show More