MINI Sh3ll
<?php
namespace MongoDB\Tests;
use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use MongoDB\Client;
use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\Query;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
use MongoDB\Driver\ServerApi;
use MongoDB\Driver\WriteConcern;
use MongoDB\Operation\CreateCollection;
use MongoDB\Operation\DatabaseCommand;
use MongoDB\Operation\DropCollection;
use MongoDB\Operation\ListCollections;
use stdClass;
use UnexpectedValueException;
use function array_merge;
use function call_user_func;
use function count;
use function current;
use function explode;
use function filter_var;
use function getenv;
use function implode;
use function is_array;
use function is_callable;
use function is_object;
use function is_string;
use function key;
use function ob_get_clean;
use function ob_start;
use function parse_url;
use function phpinfo;
use function preg_match;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function version_compare;
use const FILTER_VALIDATE_BOOLEAN;
use const INFO_MODULES;
abstract class FunctionalTestCase extends TestCase
{
/** @var Manager */
protected $manager;
/** @var array */
private $configuredFailPoints = [];
public function setUp(): void
{
parent::setUp();
$this->manager = static::createTestManager();
$this->configuredFailPoints = [];
}
public function tearDown(): void
{
$this->disableFailPoints();
parent::tearDown();
}
public static function createTestClient(?string $uri = null, array $options = [], array $driverOptions = []): Client
{
return new Client(
$uri ?? static::getUri(),
static::appendAuthenticationOptions($options),
static::appendServerApiOption($driverOptions)
);
}
public static function createTestManager(?string $uri = null, array $options = [], array $driverOptions = []): Manager
{
return new Manager(
$uri ?? static::getUri(),
static::appendAuthenticationOptions($options),
static::appendServerApiOption($driverOptions)
);
}
public static function getUri($allowMultipleMongoses = false): string
{
$uri = parent::getUri();
if ($allowMultipleMongoses) {
return $uri;
}
$urlParts = parse_url($uri);
if ($urlParts === false) {
return $uri;
}
// Only modify URIs using the mongodb scheme
if ($urlParts['scheme'] !== 'mongodb') {
return $uri;
}
$hosts = explode(',', $urlParts['host']);
$numHosts = count($hosts);
if ($numHosts === 1) {
return $uri;
}
$manager = static::createTestManager($uri);
if ($manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY))->getType() !== Server::TYPE_MONGOS) {
return $uri;
}
// Re-append port to last host
if (isset($urlParts['port'])) {
$hosts[$numHosts - 1] .= ':' . $urlParts['port'];
}
$parts = ['mongodb://'];
if (isset($urlParts['user'], $urlParts['pass'])) {
$parts += [
$urlParts['user'],
':',
$urlParts['pass'],
'@',
];
}
$parts[] = $hosts[0];
if (isset($urlParts['path'])) {
$parts[] = $urlParts['path'];
}
if (isset($urlParts['query'])) {
$parts = array_merge($parts, [
'?',
$urlParts['query'],
]);
}
return implode('', $parts);
}
protected function assertCollectionCount($namespace, $count): void
{
[$databaseName, $collectionName] = explode('.', $namespace, 2);
$cursor = $this->manager->executeCommand($databaseName, new Command(['count' => $collectionName]));
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
$document = current($cursor->toArray());
$this->assertArrayHasKey('n', $document);
$this->assertEquals($count, $document['n']);
}
/**
* Asserts that a collection with the given name does not exist on the
* server.
*
* $databaseName defaults to TestCase::getDatabaseName() if unspecified.
*/
protected function assertCollectionDoesNotExist(string $collectionName, ?string $databaseName = null): void
{
if (! isset($databaseName)) {
$databaseName = $this->getDatabaseName();
}
$operation = new ListCollections($this->getDatabaseName());
$collections = $operation->execute($this->getPrimaryServer());
$foundCollection = null;
foreach ($collections as $collection) {
if ($collection->getName() === $collectionName) {
$foundCollection = $collection;
break;
}
}
$this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName));
}
/**
* Asserts that a collection with the given name exists on the server.
*
* $databaseName defaults to TestCase::getDatabaseName() if unspecified.
* An optional $callback may be provided, which should take a CollectionInfo
* argument as its first and only parameter. If a CollectionInfo matching
* the given name is found, it will be passed to the callback, which may
* perform additional assertions.
*/
protected function assertCollectionExists(string $collectionName, ?string $databaseName = null, ?callable $callback = null): void
{
if (! isset($databaseName)) {
$databaseName = $this->getDatabaseName();
}
if ($callback !== null && ! is_callable($callback)) {
throw new InvalidArgumentException('$callback is not a callable');
}
$operation = new ListCollections($databaseName);
$collections = $operation->execute($this->getPrimaryServer());
$foundCollection = null;
foreach ($collections as $collection) {
if ($collection->getName() === $collectionName) {
$foundCollection = $collection;
break;
}
}
$this->assertNotNull($foundCollection, sprintf('Found %s collection in the database', $collectionName));
if ($callback !== null) {
call_user_func($callback, $foundCollection);
}
}
protected function assertCommandSucceeded($document): void
{
$document = is_object($document) ? (array) $document : $document;
$this->assertArrayHasKey('ok', $document);
$this->assertEquals(1, $document['ok']);
}
protected function assertSameObjectId($expectedObjectId, $actualObjectId): void
{
$this->assertInstanceOf(ObjectId::class, $expectedObjectId);
$this->assertInstanceOf(ObjectId::class, $actualObjectId);
$this->assertEquals((string) $expectedObjectId, (string) $actualObjectId);
}
/**
* Configure a fail point for the test.
*
* The fail point will automatically be disabled during tearDown() to avoid
* affecting a subsequent test.
*
* @param array|stdClass $command configureFailPoint command document
* @throws InvalidArgumentException if $command is not a configureFailPoint command
*/
public function configureFailPoint($command, ?Server $server = null): void
{
if (! $this->isFailCommandSupported()) {
$this->markTestSkipped('failCommand is only supported on mongod >= 4.0.0 and mongos >= 4.1.5.');
}
if (! $this->isFailCommandEnabled()) {
$this->markTestSkipped('The enableTestCommands parameter is not enabled.');
}
if (is_array($command)) {
$command = (object) $command;
}
if (! $command instanceof stdClass) {
throw new InvalidArgumentException('$command is not an array or stdClass instance');
}
if (key($command) !== 'configureFailPoint') {
throw new InvalidArgumentException('$command is not a configureFailPoint command');
}
$failPointServer = $server ?: $this->getPrimaryServer();
$operation = new DatabaseCommand('admin', $command);
$cursor = $operation->execute($failPointServer);
$result = $cursor->toArray()[0];
$this->assertCommandSucceeded($result);
// Record the fail point so it can be disabled during tearDown()
$this->configuredFailPoints[] = [$command->configureFailPoint, $failPointServer];
}
/**
* Creates the test collection with the specified options.
*
* If the "writeConcern" option is not specified but is supported by the
* server, a majority write concern will be used. This is helpful for tests
* using transactions or secondary reads.
*
* @param array $options
*/
protected function createCollection(array $options = []): void
{
if (version_compare($this->getServerVersion(), '3.4.0', '>=')) {
$options += ['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)];
}
$operation = new CreateCollection($this->getDatabaseName(), $this->getCollectionName(), $options);
$operation->execute($this->getPrimaryServer());
}
/**
* Drops the test collection with the specified options.
*
* If the "writeConcern" option is not specified but is supported by the
* server, a majority write concern will be used. This is helpful for tests
* using transactions or secondary reads.
*
* @param array $options
*/
protected function dropCollection(array $options = []): void
{
if (version_compare($this->getServerVersion(), '3.4.0', '>=')) {
$options += ['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)];
}
$operation = new DropCollection($this->getDatabaseName(), $this->getCollectionName(), $options);
$operation->execute($this->getPrimaryServer());
}
protected function getFeatureCompatibilityVersion(?ReadPreference $readPreference = null)
{
if ($this->isShardedCluster()) {
return $this->getServerVersion($readPreference);
}
if (version_compare($this->getServerVersion(), '3.4.0', '<')) {
return $this->getServerVersion($readPreference);
}
$cursor = $this->manager->executeCommand(
'admin',
new Command(['getParameter' => 1, 'featureCompatibilityVersion' => 1]),
$readPreference ?: new ReadPreference(ReadPreference::RP_PRIMARY)
);
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
$document = current($cursor->toArray());
// MongoDB 3.6: featureCompatibilityVersion is an embedded document
if (isset($document['featureCompatibilityVersion']['version']) && is_string($document['featureCompatibilityVersion']['version'])) {
return $document['featureCompatibilityVersion']['version'];
}
// MongoDB 3.4: featureCompatibilityVersion is a string
if (isset($document['featureCompatibilityVersion']) && is_string($document['featureCompatibilityVersion'])) {
return $document['featureCompatibilityVersion'];
}
throw new UnexpectedValueException('Could not determine featureCompatibilityVersion');
}
protected function getPrimaryServer()
{
return $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
}
protected function getServerVersion(?ReadPreference $readPreference = null)
{
$buildInfo = $this->manager->executeCommand(
$this->getDatabaseName(),
new Command(['buildInfo' => 1]),
$readPreference ?: new ReadPreference(ReadPreference::RP_PRIMARY)
)->toArray()[0];
if (isset($buildInfo->version) && is_string($buildInfo->version)) {
return preg_replace('#^(\d+\.\d+\.\d+).*$#', '\1', $buildInfo->version);
}
throw new UnexpectedValueException('Could not determine server version');
}
protected function getServerStorageEngine(?ReadPreference $readPreference = null)
{
$cursor = $this->manager->executeCommand(
$this->getDatabaseName(),
new Command(['serverStatus' => 1]),
$readPreference ?: new ReadPreference('primary')
);
$result = current($cursor->toArray());
if (isset($result->storageEngine->name) && is_string($result->storageEngine->name)) {
return $result->storageEngine->name;
}
throw new UnexpectedValueException('Could not determine server storage engine');
}
protected function isLoadBalanced()
{
return $this->getPrimaryServer()->getType() == Server::TYPE_LOAD_BALANCER;
}
protected function isReplicaSet()
{
return $this->getPrimaryServer()->getType() == Server::TYPE_RS_PRIMARY;
}
protected function isMongos()
{
return $this->getPrimaryServer()->getType() == Server::TYPE_MONGOS;
}
/**
* Return whether serverless (i.e. proxy as mongos) is being utilized.
*/
protected static function isServerless(): bool
{
$isServerless = getenv('MONGODB_IS_SERVERLESS');
return $isServerless !== false ? filter_var($isServerless, FILTER_VALIDATE_BOOLEAN) : false;
}
protected function isShardedCluster()
{
$type = $this->getPrimaryServer()->getType();
if ($type == Server::TYPE_MONGOS) {
return true;
}
// Assume that load balancers are properly configured and front mongos
if ($type == Server::TYPE_LOAD_BALANCER) {
return true;
}
return false;
}
protected function isShardedClusterUsingReplicasets()
{
$cursor = $this->getPrimaryServer()->executeQuery(
'config.shards',
new Query([], ['limit' => 1])
);
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
$document = current($cursor->toArray());
if (! $document) {
return false;
}
/**
* Use regular expression to distinguish between standalone or replicaset:
* Without a replicaset: "host" : "localhost:4100"
* With a replicaset: "host" : "dec6d8a7-9bc1-4c0e-960c-615f860b956f/localhost:4400,localhost:4401"
*/
return preg_match('@^.*/.*:\d+@', $document['host']);
}
protected function skipIfChangeStreamIsNotSupported(): void
{
switch ($this->getPrimaryServer()->getType()) {
case Server::TYPE_MONGOS:
case Server::TYPE_LOAD_BALANCER:
if (version_compare($this->getServerVersion(), '3.6.0', '<')) {
$this->markTestSkipped('$changeStream is only supported on MongoDB 3.6 or higher');
}
if (! $this->isShardedClusterUsingReplicasets()) {
$this->markTestSkipped('$changeStream is only supported with replicasets');
}
break;
case Server::TYPE_RS_PRIMARY:
if (version_compare($this->getFeatureCompatibilityVersion(), '3.6', '<')) {
$this->markTestSkipped('$changeStream is only supported on FCV 3.6 or higher');
}
break;
default:
$this->markTestSkipped('$changeStream is not supported');
}
}
protected function skipIfCausalConsistencyIsNotSupported(): void
{
switch ($this->getPrimaryServer()->getType()) {
case Server::TYPE_MONGOS:
case Server::TYPE_LOAD_BALANCER:
if (version_compare($this->getServerVersion(), '3.6.0', '<')) {
$this->markTestSkipped('Causal Consistency is only supported on MongoDB 3.6 or higher');
}
if (! $this->isShardedClusterUsingReplicasets()) {
$this->markTestSkipped('Causal Consistency is only supported with replicasets');
}
break;
case Server::TYPE_RS_PRIMARY:
if (version_compare($this->getFeatureCompatibilityVersion(), '3.6', '<')) {
$this->markTestSkipped('Causal Consistency is only supported on FCV 3.6 or higher');
}
if ($this->getServerStorageEngine() !== 'wiredTiger') {
$this->markTestSkipped('Causal Consistency requires WiredTiger storage engine');
}
break;
default:
$this->markTestSkipped('Causal Consistency is not supported');
}
}
protected function skipIfClientSideEncryptionIsNotSupported(): void
{
if (version_compare($this->getFeatureCompatibilityVersion(), '4.2', '<')) {
$this->markTestSkipped('Client Side Encryption only supported on FCV 4.2 or higher');
}
if ($this->getModuleInfo('libmongocrypt') === 'disabled') {
$this->markTestSkipped('Client Side Encryption is not enabled in the MongoDB extension');
}
}
protected function skipIfGeoHaystackIndexIsNotSupported(): void
{
if (version_compare($this->getServerVersion(), '4.9', '>=')) {
$this->markTestSkipped('GeoHaystack indexes cannot be created in version 4.9 and above');
}
}
protected function skipIfTransactionsAreNotSupported(): void
{
if ($this->getPrimaryServer()->getType() === Server::TYPE_STANDALONE) {
$this->markTestSkipped('Transactions are not supported on standalone servers');
}
if ($this->isShardedCluster()) {
if (! $this->isShardedClusterUsingReplicasets()) {
$this->markTestSkipped('Transactions are not supported on sharded clusters without replica sets');
}
if (version_compare($this->getFeatureCompatibilityVersion(), '4.2', '<')) {
$this->markTestSkipped('Transactions are only supported on FCV 4.2 or higher');
}
return;
}
if (version_compare($this->getFeatureCompatibilityVersion(), '4.0', '<')) {
$this->markTestSkipped('Transactions are only supported on FCV 4.0 or higher');
}
if ($this->getServerStorageEngine() !== 'wiredTiger') {
$this->markTestSkipped('Transactions require WiredTiger storage engine');
}
}
private static function appendAuthenticationOptions(array $options): array
{
if (isset($options['username']) || isset($options['password'])) {
return $options;
}
$username = getenv('MONGODB_USERNAME') ?: null;
$password = getenv('MONGODB_PASSWORD') ?: null;
if ($username !== null) {
$options['username'] = $username;
}
if ($password !== null) {
$options['password'] = $password;
}
return $options;
}
private static function appendServerApiOption(array $driverOptions): array
{
if (getenv('API_VERSION') && ! isset($driverOptions['serverApi'])) {
$driverOptions['serverApi'] = new ServerApi(getenv('API_VERSION'));
}
return $driverOptions;
}
/**
* Disables any fail points that were configured earlier in the test.
*
* This tracks fail points set via configureFailPoint() and should be called
* during tearDown().
*/
private function disableFailPoints(): void
{
if (empty($this->configuredFailPoints)) {
return;
}
foreach ($this->configuredFailPoints as [$failPoint, $server]) {
$operation = new DatabaseCommand('admin', ['configureFailPoint' => $failPoint, 'mode' => 'off']);
$operation->execute($server);
}
}
private function getModuleInfo(string $row): ?string
{
ob_start();
phpinfo(INFO_MODULES);
$info = ob_get_clean();
$pattern = sprintf('/^%s([\w ]+)$/m', preg_quote($row . ' => '));
if (preg_match($pattern, $info, $matches) !== 1) {
return null;
}
return $matches[1];
}
/**
* Checks if the failCommand command is supported on this server version
*
* @return bool
*/
private function isFailCommandSupported(): bool
{
$minVersion = $this->isShardedCluster() ? '4.1.5' : '4.0.0';
return version_compare($this->getServerVersion(), $minVersion, '>=');
}
/**
* Checks if the failCommand command is enabled by checking the enableTestCommands parameter
*
* @return bool
*/
private function isFailCommandEnabled(): bool
{
try {
$cursor = $this->manager->executeCommand(
'admin',
new Command(['getParameter' => 1, 'enableTestCommands' => 1])
);
$document = current($cursor->toArray());
} catch (CommandException $e) {
return false;
}
return isset($document->enableTestCommands) && $document->enableTestCommands === true;
}
}
OHA YOOOO