...
 
Commits (2)
......@@ -104,8 +104,8 @@ class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
Security\XSRF::setCookie(true);
// Set the canary cookie
Di::_()->get('Features\Manager')
->setCanaryCookie($user->isCanary());
Di::_()->get('Features\Canary')
->setCookie($user->isCanary());
$response['status'] = 'success';
$response['user'] = $user->export();
......
......@@ -24,8 +24,8 @@ class canary implements Interfaces\Api
}
// Refresh the canary cookie
Di::_()->get('Features\Manager')
->setCanaryCookie($user->isCanary());
Di::_()->get('Features\Canary')
->setCookie($user->isCanary());
return Factory::response([
'enabled' => (bool) $user->isCanary(),
......@@ -57,8 +57,8 @@ class canary implements Interfaces\Api
]);
// Set the canary cookie
Di::_()->get('Features\Manager')
->setCanaryCookie($user->isCanary());
Di::_()->get('Features\Canary')
->setCookie($user->isCanary());
return Factory::response([]);
}
......
......@@ -8,6 +8,7 @@ namespace Minds\Core\Config;
use Minds\Core\Blockchain\Manager as BlockchainManager;
use Minds\Core\Di\Di;
use Minds\Core\Features\Manager as FeaturesManager;
use Minds\Core\I18n\I18n;
use Minds\Core\Navigation\Manager as NavigationManager;
use Minds\Core\Rewards\Contributions\ContributionValues;
......@@ -27,23 +28,29 @@ class Exported
/** @var I18n */
protected $i18n;
/** @var FeaturesManager */
protected $features;
/**
* Exported constructor.
* @param Config $config
* @param ThirdPartyNetworksManager $thirdPartyNetworks
* @param I18n $i18n
* @param BlockchainManager $blockchain
* @param FeaturesManager $features
*/
public function __construct(
$config = null,
$thirdPartyNetworks = null,
$i18n = null,
$blockchain = null
$blockchain = null,
$features = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->thirdPartyNetworks = $thirdPartyNetworks ?: Di::_()->get('ThirdPartyNetworks\Manager');
$this->i18n = $i18n ?: Di::_()->get('I18n');
$this->blockchain = $blockchain ?: Di::_()->get('Blockchain\Manager');
$this->features = $features ?: Di::_()->get('Features\Manager');
}
/**
......@@ -71,7 +78,7 @@ class Exported
'recaptchaKey' => $this->config->get('google')['recaptcha']['site_key'],
'max_video_length' => $this->config->get('max_video_length'),
'max_video_file_size' => $this->config->get('max_video_file_size'),
'features' => (object) ($this->config->get('features') ?: []),
'features' => (object) ($this->features->export() ?: []),
'blockchain' => (object) $this->blockchain->getPublicSettings(),
'sale' => $this->config->get('blockchain')['sale'],
'last_tos_update' => $this->config->get('last_tos_update') ?: time(),
......
......@@ -25,6 +25,9 @@ class DataProvider extends Provider
$this->di->bind('Cache\Apcu', function ($di) {
return new cache\apcu();
}, ['useFactory'=>true]);
$this->di->bind('Cache\PsrWrapper', function ($di) {
return new cache\PsrWrapper();
}, ['useFactory'=>true]);
/**
* Database bindings
*/
......
<?php
/**
* PsrWrapper
*
* @author edgebal
*/
namespace Minds\Core\Data\cache;
use Minds\Core\Di\Di;
use NotImplementedException;
use Psr\SimpleCache\CacheInterface;
class PsrWrapper implements CacheInterface
{
/** @var abstractCacher */
protected $cache;
/**
* PsrWrapper constructor.
* @param abstractCacher $cache
*/
public function __construct(
$cache = null
) {
$this->cache = $cache ?: Di::_()->get('Cache');
}
/**
* @inheritDoc
*/
public function get($key, $default = null)
{
return $this->cache->get($key) ?? $default;
}
/**
* @inheritDoc
*/
public function set($key, $value, $ttl = null)
{
return $this->cache->set($key, $value, $ttl);
}
/**
* @inheritDoc
*/
public function delete($key)
{
$this->cache->destroy($key);
}
/**
* @inheritDoc
* @throws NotImplementedException
*/
public function clear()
{
throw new NotImplementedException();
}
/**
* @inheritDoc
*/
public function getMultiple($keys, $default = null)
{
$values = [];
foreach ($keys as $key) {
$values[$key] = $this->cache->get($key) ?? $default;
}
return $values;
}
/**
* @inheritDoc
*/
public function setMultiple($values, $ttl = null)
{
foreach ($values as $key => $value) {
$this->cache->set($key, $value, $ttl);
}
return true;
}
/**
* @inheritDoc
*/
public function deleteMultiple($keys)
{
foreach ($keys as $key) {
$this->cache->destroy($key);
}
return true;
}
/**
* @inheritDoc
*/
public function has($key)
{
return $this->cache->get($key) !== null;
}
}
<?php
/**
* Canary
*
* @author edgebal
*/
namespace Minds\Core\Features;
use Minds\Common\Cookie;
/**
* Controls Canary cookie setting
* @package Minds\Core\Features
*/
class Canary
{
/** @var Cookie $cookie */
protected $cookie;
/**
* Canary constructor.
* @param Cookie $cookie
*/
public function __construct(
$cookie = null
) {
$this->cookie = $cookie ?: new Cookie();
}
/**
* Sets canary cookie value
* @param bool $enabled
* @return bool
*/
public function setCookie(bool $enabled): bool
{
$this->cookie
->setName('canary')
->setValue((int) $enabled)
->setExpire(0)
->setSecure(true) //only via ssl
->setHttpOnly(true) //never by browser
->setPath('/')
->create();
return true;
}
}
<?php
/**
* FeatureNotImplementedException
*
* @author edgebal
*/
namespace Minds\Core\Features\Exceptions;
use Exception;
class FeatureNotImplementedException extends Exception
{
}
<?php
/**
* Minds Features Provider
*
* @author emi
*/
namespace Minds\Core\Features;
use Minds\Core\Di\Provider;
class FeaturesProvider extends Provider
{
public function register()
{
$this->di->bind('Features', function ($di) {
return new Manager();
}, [ 'useFactory'=> true ]);
$this->di->bind('Features\Manager', function ($di) {
return new Manager();
}, [ 'useFactory'=> true ]);
}
}
......@@ -9,87 +9,90 @@
namespace Minds\Core\Features;
use Minds\Core\Di\Di;
use Minds\Common\Cookie;
use Minds\Core\Session;
use Minds\Core\Features\Exceptions\FeatureNotImplementedException;
use Minds\Core\Sessions\ActiveSession;
/**
* Features Manager
* @package Minds\Core\Features
*/
class Manager
{
/** @var User $user */
private $user;
/** @var Services\ServiceInterface[] */
protected $services;
/** @var Config $config */
private $config;
/** @var ActiveSession */
protected $activeSession;
/** @var Cookie $cookie */
private $cookie;
public function __construct($config = null, $cookie = null, $user = null)
{
$this->config = $config ?: Di::_()->get('Config');
$this->cookie = $cookie ?: new Cookie;
$this->user = $user ?? Session::getLoggedInUser();
}
/** @var string[] */
protected $featureKeys;
/**
* Set the user
* @param User $user
* @return $this
* Manager constructor.
* @param Services\ServiceInterface[] $services
* @param ActiveSession $activeSession
* @param string[] $features
*/
public function setUser($user)
{
$this->user = $user;
return $this;
public function __construct(
$services = null,
$activeSession = null,
array $features = null
) {
$this->services = $services ?: [
new Services\Config(),
new Services\Unleash(),
new Services\Environment(),
];
$this->activeSession = $activeSession ?: Di::_()->get('Sessions\ActiveSession');
$this->featureKeys = ($features ?? Di::_()->get('Features\Keys')) ?: [];
}
/**
* Checks if a featured is enabled
* @param $feature
* Checks if a feature is enabled
* @param string $feature
* @return bool
* @throws FeatureNotImplementedException
*/
public function has($feature)
public function has(string $feature): ?bool
{
$features = $this->config->get('features') ?: [];
$features = $this->export();
if (!isset($features[$feature])) {
// error_log("[Features\Manager] Feature '{$feature}' is not declared. Assuming false.");
return false;
}
if ($features[$feature] === 'admin' && $this->user->isAdmin()) {
return true;
}
if ($features[$feature] === 'canary' && $this->user && $this->user->get('canary')) {
return true;
throw new FeatureNotImplementedException(
"${feature}: Not Implemented"
);
}
return $features[$feature] === true;
return (bool) $features[$feature];
}
/**
* Exports the features array
* Exports the whole features array based on Features DI
* @return array
*/
public function export()
public function export(): array
{
return $this->config->get('features') ?: [];
}
$features = [];
/**
* Set the canary cookie
* @param bool $enabled
* @return void
*/
public function setCanaryCookie(bool $enabled = true) : void
{
$this->cookie
->setName('canary')
->setValue((int) $enabled)
->setExpire(0)
->setSecure(true) //only via ssl
->setHttpOnly(true) //never by browser
->setPath('/')
->create();
// Initialize array with false values
foreach ($this->featureKeys as $feature) {
$features[$feature] = false;
}
// Fetch from every service
foreach ($this->services as $service) {
$features = array_merge(
$features,
$service
->setUser($this->activeSession->getUser())
->fetch($this->featureKeys)
);
}
//
return $features;
}
}
<?php
/**
* Module
*
* @author edgebal
*/
namespace Minds\Core\Features;
use Minds\Interfaces\ModuleInterface;
class Module implements ModuleInterface
{
/**
* @inheritDoc
*/
public function onInit()
{
(new Provider())->register();
}
}
<?php
/**
* Minds Features Provider
*
* @author emi
*/
namespace Minds\Core\Features;
use Minds\Core\Di\Provider as DiProvider;
/**
* Features provider
* @package Minds\Core\Features
*/
class Provider extends DiProvider
{
public function register()
{
$this->di->bind('Features\Keys', function () {
return [
'psr7-router',
'es-feeds',
'helpdesk',
'top-feeds',
'cassandra-notifications',
'dark-mode',
'allow-comments-toggle',
'permissions',
'pro',
'webtorrent',
'top-feeds-by-age',
'homepage-december-2019',
'onboarding-december-2019',
'register_pages-december-2019',
'modal-pager',
'blockchain_creditcard',
'channel-filter-feeds',
'suggested-users',
'top-feeds-filter',
'media-modal',
'wire-multi-currency',
'cdn-jwt',
'post-scheduler',
];
});
$this->di->bind('Features\Manager', function ($di) {
return new Manager();
}, [ 'useFactory'=> true ]);
$this->di->bind('Features\Canary', function ($di) {
return new Canary();
}, [ 'useFactory'=> true ]);
}
}
<?php
/**
* BaseService
*
* @author edgebal
*/
namespace Minds\Core\Features\Services;
use Minds\Entities\User;
/**
* Base service to be used when building integrations
* @package Minds\Core\Features\Services
*/
abstract class BaseService implements ServiceInterface
{
/** @var User */
protected $user;
/**
* @inheritDoc
* @param User|null $user
* @return ServiceInterface
*/
public function setUser(?User $user): ServiceInterface
{
$this->user = $user;
return $this;
}
/**
* Calculate user groups based on their state
* @return array
*/
public function getUserGroups(): array
{
$groups = [];
if (!$this->user) {
$groups[] = 'anonymous';
} else {
$groups[] = 'authenticated';
if ($this->user->isAdmin()) {
$groups[] = 'admin';
}
if ($this->user->isCanary()) {
$groups[] = 'canary';
}
if ($this->user->isPro()) {
$groups[] = 'pro';
}
if ($this->user->isPlus()) {
$groups[] = 'plus';
}
}
return $groups;
}
}
<?php
/**
* Config
*
* @author edgebal
*/
namespace Minds\Core\Features\Services;
use InvalidArgumentException;
use Minds\Core\Config as MindsConfig;
use Minds\Core\Di\Di;
/**
* Static config (settings.php) feature flags service
* @package Minds\Core\Features\Services
*/
class Config extends BaseService
{
/** @var MindsConfig */
protected $config;
/**
* Config constructor.
* @param Config $config
*/
public function __construct(
$config = null
) {
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @inheritDoc
*/
public function fetch(array $keys): array
{
// Return whitelisted 'features' array with its values resolved
return array_intersect_key(
array_map(
[$this, '_resolveValue'],
$this->config->get('features') ?: []
),
array_flip($keys)
);
}
/**
* Resolve strings to groups. Boolean are returned as is. Other types throw an exception.
* @param mixed $value
* @return bool
* @throws InvalidArgumentException
*/
protected function _resolveValue($value): bool
{
if (is_string($value)) {
return in_array(strtolower($value), $this->getUserGroups(), true);
} elseif (is_bool($value)) {
return $value;
}
throw new InvalidArgumentException();
}
}
<?php
/**
* Environment
*
* @author edgebal
*/
namespace Minds\Core\Features\Services;
/**
* Environment variables ($_ENV) feature flags service
* @package Minds\Core\Features\Services
*/
class Environment extends BaseService
{
/** @var array|null */
protected $global = null;
/**
* @param array $global
* @return Environment
*/
public function setGlobal(?array $global): Environment
{
$this->global = $global;
return $this;
}
/**
* @inheritDoc
*/
public function fetch(array $keys): array
{
$output = [];
$global = $this->global ?? $_ENV;
foreach ($keys as $key) {
// Convert to variable name
// Example: `webtorrent` would be `MINDS_FEATURE_WEBTORRENT`; `psr7-router` would be `MINDS_FEATURE_PSR7_ROUTER`
$envName = sprintf('MINDS_FEATURE_%s', strtoupper(preg_replace('/[^a-zA-Z0-9]+/', '_', $key)));
if (isset($global[$envName])) {
// Read value as string
$value = (string) $global[$envName];
// Resolve group, if not 0 or 1
if (strlen($value) > 0 && $value !== '0' && $value !== '1') {
$value = in_array(strtolower($value), $this->getUserGroups(), true);
}
// Set value
$output[$key] = (bool) $value;
}
}
return $output;
}
}
<?php
/**
* ServiceInterface
*
* @author edgebal
*/
namespace Minds\Core\Features\Services;
use Minds\Entities\User;
interface ServiceInterface
{
/**
* Sets the current user to calculate context values
* @param User|null $user
* @return ServiceInterface
*/
public function setUser(?User $user): ServiceInterface;
/**
* Fetches the whole feature flag set
* @param string[] $keys Array of whitelisted keys
* @return array
*/
public function fetch(array $keys): array;
}
<?php
/**
* Unleash
*
* @author edgebal
*/
namespace Minds\Core\Features\Services;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\UnleashClient\Config as UnleashConfig;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Unleash as UnleashClient;
/**
* Unleash server (GitLab FF) feature flags service
* @package Minds\Core\Features\Services
*/
class Unleash extends BaseService
{
/** @var Config */
protected $config;
/** @var UnleashClient */
protected $unleash;
/**
* Unleash constructor.
* @param Config $config
* @param UnleashClient $unleash
*/
public function __construct(
$config = null,
$unleash = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->unleash = $unleash ?: $this->initUnleashClient();
}
/**
* Initializes Unleash client package
* @return UnleashClient
*/
public function initUnleashClient(): UnleashClient
{
$configValues = $this->config->get('unleash');
$config = new UnleashConfig(
getenv('UNLEASH_API_URL') ?: ($configValues['apiUrl'] ?? null),
getenv('UNLEASH_INSTANCE_ID') ?: ($configValues['instanceId'] ?? null),
getenv('MINDS_ENV') ?: ($configValues['applicationName'] ?? 'development'),
$configValues['pollingIntervalSeconds'] ?? null,
$configValues['metricsIntervalSeconds'] ?? null
);
$logger = Di::_()->get('Logger\Singleton');
$cache = Di::_()->get('Cache\PsrWrapper');
return new UnleashClient($config, $logger, null, $cache);
}
/**
* @inheritDoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function fetch(array $keys): array
{
$context = new Context();
$context
->setUserGroups($this->getUserGroups())
->setRemoteAddress($_SERVER['REMOTE_ADDR'] ?? '')
->setHostName($_SERVER['HTTP_HOST'] ?? '');
if ($this->user) {
$context
->setUserId((string) $this->user->guid);
}
// Return whitelisted 'features' array with its values resolved
return array_intersect_key(
$this->unleash
->setContext($context)
->export(),
array_flip($keys)
);
}
}
......@@ -38,7 +38,7 @@ class Repository
$config = $config ?: Di::_()->get('Config');
$this->features = $features ?: Di::_()->get('Features');
$this->features = $features ?: Di::_()->get('Features\Manager');
$this->index = $config->get('elasticsearch')['index'];
}
......
......@@ -36,13 +36,17 @@ class Logger extends MonologLogger
$options = array_merge([
'isProduction' => true,
'devToolsLogger' => '',
'minLogLevel' => null,
], $options);
$isProduction = (bool) $options['isProduction'];
$level = $options['minLogLevel'] ?? MonologLogger::WARNING;
$handlers = [];
$errorLogHandler = new ErrorLogHandler(
ErrorLogHandler::OPERATING_SYSTEM,
$options['isProduction'] ? MonologLogger::INFO : MonologLogger::DEBUG,
$level,
true,
true
);
......@@ -51,29 +55,30 @@ class Logger extends MonologLogger
->setFormatter(new LineFormatter(
"%channel%.%level_name%: %message% %context% %extra%\n",
'c',
!$options['isProduction'], // Allow newlines on dev mode
!$isProduction, // Allow newlines on dev mode
true
));
$handlers[] = $errorLogHandler;
if ($options['isProduction']) {
$handlers[] = new SentryHandler(SentrySdk::getCurrentHub());
if ($isProduction) {
// Do _NOT_ send INFO or DEBUG
$handlers[] = new SentryHandler(SentrySdk::getCurrentHub(), max($level, MonologLogger::WARNING));
} else {
// Extra handlers for Development Mode
switch ($options['devToolsLogger']) {
case 'firephp':
$handlers[] = new FirePHPHandler();
$handlers[] = new FirePHPHandler($level);
break;
case 'chromelogger':
$handlers[] = new ChromePHPHandler();
$handlers[] = new ChromePHPHandler($level);
break;
case 'phpconsole':
try {
$handlers[] = new PHPConsoleHandler();
$handlers[] = new PHPConsoleHandler(null, null, $level);
} catch (Exception $exception) {
// If the server-side vendor package is not installed, ignore any warnings.
}
......
......@@ -29,6 +29,7 @@ class Provider extends DiProvider
$options = [
'isProduction' => $config ? !$config->get('development_mode') : true,
'devToolsLogger' => $config ? $config->get('devtools_logger') : '',
'minLogLevel' => $config ? $config->get('min_log_level') : null,
];
return new Logger('Minds', $options);
......
......@@ -18,6 +18,7 @@ class Minds extends base
private $modules = [
Log\Module::class,
Events\Module::class,
Features\Module::class,
SSO\Module::class,
Email\Module::class,
Experiments\Module::class,
......@@ -105,7 +106,6 @@ class Minds extends base
(new Groups\GroupsProvider())->register();
(new Search\SearchProvider())->register();
(new Votes\VotesProvider())->register();
(new Features\FeaturesProvider())->register();
(new SMS\SMSProvider())->register();
(new Blockchain\BlockchainProvider())->register();
(new Issues\IssuesProvider())->register();
......
......@@ -37,7 +37,7 @@ class Router
$fallback = null
) {
$this->dispatcher = $dispatcher ?: Di::_()->get('Router');
$this->features = $features ?: Di::_()->get('Features');
$this->features = $features ?: Di::_()->get('Features\Manager');
$this->fallback = $fallback ?: new Fallback();
}
......
......@@ -20,7 +20,7 @@ class Hot implements SortingAlgorithm
public function __construct($features = null)
{
$this->features = $features ?? Di::_()->get('Features');
$this->features = $features ?? Di::_()->get('Features\Manager');
}
......
<?php
/**
* ActiveSession
*
* @author edgebal
*/
namespace Minds\Core\Sessions;
use Minds\Core\Session as CoreSession;
use Minds\Entities\User;
/**
* Allow using an instance-based dependency to retrieve
* the currently logged-in user
* @package Minds\Core\Sessions
*/
class ActiveSession
{
/**
* Gets the currently logged in user
* @return User|null
*/
public function getUser(): ?User
{
return CoreSession::getLoggedinUser() ?: null;
}
}
......@@ -15,5 +15,9 @@ class SessionsProvider extends Provider
$this->di->bind('Sessions\Manager', function ($di) {
return new Manager;
}, ['useFactory'=>true]);
$this->di->bind('Sessions\ActiveSession', function ($di) {
return new ActiveSession();
}, ['useFactory'=>true]);
}
}
<?php
namespace Spec\Minds\Core\Features;
use Minds\Common\Cookie;
use Minds\Core\Features\Canary;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class CanarySpec extends ObjectBehavior
{
/** @var Cookie */
protected $cookie;
public function let(
Cookie $cookie
) {
$this->cookie = $cookie;
$this->beConstructedWith($cookie);
}
public function it_is_initializable()
{
$this->shouldHaveType(Canary::class);
}
public function it_should_set_cookie_enabled()
{
$this->cookie->setName('canary')
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setValue(1)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setExpire(0)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setSecure(true)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setHttpOnly(true)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setPath('/')
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->create()
->shouldBeCalled()
->willReturn(true);
$this
->setCookie(true)
->shouldReturn(true);
}
public function it_should_set_cookie_disabled()
{
$this->cookie->setName('canary')
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setValue(0)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setExpire(0)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setSecure(true)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setHttpOnly(true)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setPath('/')
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->create()
->shouldBeCalled()
->willReturn(true);
$this
->setCookie(false)
->shouldReturn(true);
}
}
......@@ -4,19 +4,38 @@ namespace Spec\Minds\Core\Features;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Features\Exceptions\FeatureNotImplementedException;
use Minds\Core\Features\Manager;
use Minds\Core\Features\Services\ServiceInterface;
use Minds\Core\Sessions\ActiveSession;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
class ManagerSpec extends ObjectBehavior
{
/** @var Config */
protected $config;
public function let(Config $config)
{
$this->beConstructedWith($config);
$this->config = $config;
/** @var ServiceInterface */
protected $service1;
/** @var ServiceInterface */
protected $service2;
/** @var ActiveSession */
protected $activeSession;
public function let(
ServiceInterface $service1,
ServiceInterface $service2,
ActiveSession $activeSession
) {
$this->service1 = $service1;
$this->service2 = $service2;
$this->activeSession = $activeSession;
$this->beConstructedWith(
[ $service1, $service2 ],
$activeSession,
['feature1', 'feature2', 'feature3']
);
}
public function it_is_initializable()
......@@ -24,72 +43,142 @@ class ManagerSpec extends ObjectBehavior
$this->shouldHaveType(Manager::class);
}
public function it_should_check_if_a_feature_exists_unsuccessfully_and_assume_its_inactive()
{
$this->config->get('features')
public function it_should_throw_during_has_if_a_feature_does_not_exist(
User $user
) {
$this->activeSession->getUser()
->shouldBeCalled()
->willReturn(['plus' => true, 'wire' => false]);
->willReturn($user);
$this->has('boost')->shouldReturn(false);
}
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
public function it_should_check_if_a_feature_exists_and_return_its_deactivated()
{
$this->config->get('features')
$this->service1->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn(['plus' => true, 'wire' => false]);
->willReturn([
'feature1' => true,
'feature2' => false,
]);
$this->has('wire')->shouldReturn(false);
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn([
'feature2' => true,
]);
$this
->shouldThrow(FeatureNotImplementedException::class)
->duringHas('feature99-non-existant');
}
public function it_should_check_if_a_user_is_active_for_an_admin_and_return_true(User $user)
{
$user->isAdmin()
public function it_should_return_false_if_a_feature_exists_and_it_is_deactivated(
User $user
) {
$this->activeSession->getUser()
->shouldBeCalled()
->willReturn(true);
->willReturn($user);
//remove ip whitelist check
$_SERVER['HTTP_X_FORWARDED_FOR'] = '10.56.0.1';
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
$this->setUser($user);
$this->service1->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn([
'feature1' => true,
'feature2' => false,
]);
$this->config->get('features')
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn(['plus' => true, 'wire' => 'admin']);
->willReturn($this->service2);
$this->has('wire')->shouldReturn(true);
$this->service2->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn([
'feature2' => true,
'feature3' => false,
]);
$this
->has('feature3')
->shouldReturn(false);
}
public function it_should_check_if_a_user_is_active_for_an_admin_and_return_false(User $user)
{
$user->guid = '1234';
$user->admin = false;
public function it_should_return_true_if_a_feature_exists_and_it_is_activated(
User $user
) {
$this->activeSession->getUser()
->shouldBeCalled()
->willReturn($user);
$this->setUser($user);
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
//$this->config->get('last_tos_update')
// ->shouldBeCalled()
// ->willReturn(123456);
$this->service1->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn([
'feature1' => true,
'feature2' => false,
]);
$this->config->get('features')
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn(['plus' => true, 'wire' => 'admin']);
->willReturn($this->service2);
$this->has('wire')->shouldReturn(false);
$this->service2->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn([
'feature2' => true,
'feature3' => false,
]);
$this
->has('feature2')
->shouldReturn(true);
}
public function it_should_export_all_features()
{
$features = [
'plus' => true,
'wire' => 'admin',
'boost' => false,
];
public function it_should_export_a_merge_of_all_features(
User $user
) {
$this->activeSession->getUser()
->shouldBeCalled()
->willReturn($user);
$this->service1->setUser($user)
->shouldBeCalled()
->willReturn($this->service1);
$this->config->get('features')
$this->service1->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn($features);
->willReturn([
'feature1' => true,
'feature2' => false,
]);
$this->export()->shouldReturn($features);
$this->service2->setUser($user)
->shouldBeCalled()
->willReturn($this->service2);
$this->service2->fetch(['feature1', 'feature2', 'feature3'])
->shouldBeCalled()
->willReturn([
'feature2' => true,
'feature3' => false,
]);
$this
->export()
->shouldReturn([
'feature1' => true,
'feature2' => true,
'feature3' => false,
]);
}
}
<?php
namespace Spec\Minds\Core\Features\Services;
use Minds\Core\Config as CoreConfig;
use Minds\Core\Features\Services\Config;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ConfigSpec extends ObjectBehavior
{
/** @var CoreConfig */
protected $config;
public function let(
CoreConfig $config
) {
$this->config = $config;
$this->beConstructedWith($config);
}
public function it_is_initializable()
{
$this->shouldHaveType(Config::class);
}
public function it_should_fetch(
User $user1,
User $user2
) {
$this->config->get('features')
->shouldBeCalled()
->willReturn([
'feature1' => true,
'feature2' => false,
'feature3' => 'admin',
'feature4' => 'canary',
'feature5' => 'plus',
'feature6' => 'pro',
'unused-feature' => true,
]);
$this
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => false,
'feature4' => false,
'feature5' => false,
'feature6' => false,
]);
$user1->isCanary()
->willReturn(true);
$user1->isAdmin()
->willReturn(true);
$user1->isPlus()
->willReturn(false);
$user1->isPro()
->willReturn(false);
$this
->setUser($user1)
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => true,
'feature4' => true,
'feature5' => false,
'feature6' => false,
]);
$user2->isCanary()
->willReturn(false);
$user2->isAdmin()
->willReturn(false);
$user2->isPlus()
->willReturn(true);
$user2->isPro()
->willReturn(true);
$this
->setUser($user2)
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => false,
'feature4' => false,
'feature5' => true,
'feature6' => true,
]);
}
}
<?php
namespace Spec\Minds\Core\Features\Services;
use Minds\Core\Features\Services\Environment;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class EnvironmentSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(Environment::class);
}
public function it_should_fetch(
User $user1,
User $user2
) {
$global = [
'MINDS_FEATURE_FEATURE1' => '1',
'MINDS_FEATURE_FEATURE2' => '0',
'MINDS_FEATURE_FEATURE3' => 'admin',
'MINDS_FEATURE_FEATURE4' => 'canary',
'MINDS_FEATURE_FEATURE5' => 'plus',
'MINDS_FEATURE_FEATURE_6' => 'pro',
'MINDS_FEATURE_UNUSED_FEATURE' => '1',
];
$this
->setGlobal($global)
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature-6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => false,
'feature4' => false,
'feature5' => false,
'feature-6' => false,
]);
$user1->isCanary()
->willReturn(true);
$user1->isAdmin()
->willReturn(true);
$user1->isPlus()
->willReturn(false);
$user1->isPro()
->willReturn(false);
$this
->setUser($user1)
->setGlobal($global)
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature-6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => true,
'feature4' => true,
'feature5' => false,
'feature-6' => false,
]);
$user2->isCanary()
->willReturn(false);
$user2->isAdmin()
->willReturn(false);
$user2->isPlus()
->willReturn(true);
$user2->isPro()
->willReturn(true);
$this
->setUser($user2)
->setGlobal($global)
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature-6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => false,
'feature4' => false,
'feature5' => true,
'feature-6' => true,
]);
}
}
<?php
namespace Spec\Minds\Core\Features\Services;
use Minds\Core\Config;
use Minds\Core\Features\Services\Unleash;
use Minds\Entities\User;
use Minds\UnleashClient\Entities\Context;
use Minds\UnleashClient\Unleash as UnleashClient;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class UnleashSpec extends ObjectBehavior
{
/** @var Config */
protected $config;
/** @var UnleashClient */
protected $unleash;
public function let(
Config $config,
UnleashClient $unleash
) {
$this->config = $config;
$this->unleash = $unleash;
$this->beConstructedWith($config, $unleash);
}
public function it_is_initializable()
{
$this->shouldHaveType(Unleash::class);
}
public function it_should_fetch()
{
$this->unleash->setContext(Argument::that(function (Context $context) {
return
$context->getUserId() === null &&
$context->getUserGroups() === ['anonymous']
;
}))
->shouldBeCalled()
->willReturn($this->unleash);
$this->unleash->export()
->shouldBeCalled()
->willReturn([
'feature1' => true,
'feature2' => false,
'feature3' => true,
'feature4' => true,
'feature5' => false,
'feature6' => false,
'unused-feature' => true,
]);
$this
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => true,
'feature4' => true,
'feature5' => false,
'feature6' => false,
]);
}
public function it_should_fetch_with_user(
User $user
) {
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
$user->isAdmin()
->shouldBeCalled()
->willReturn(true);
$user->isCanary()
->shouldBeCalled()
->willReturn(true);
$user->isPlus()
->shouldBeCalled()
->willReturn(true);
$user->isPro()
->shouldBeCalled()
->willReturn(true);
$this->unleash->setContext(Argument::that(function (Context $context) {
return
$context->getUserId() === '1000' &&
$context->getUserGroups() === ['authenticated', 'admin', 'canary', 'pro', 'plus']
;
}))
->shouldBeCalled()
->willReturn($this->unleash);
$this->unleash->export()
->shouldBeCalled()
->willReturn([
'feature1' => true,
'feature2' => false,
'feature3' => true,
'feature4' => true,
'feature5' => false,
'feature6' => false,
'unused-feature' => true,
]);
$this
->setUser($user)
->fetch([
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature6',
])
->shouldReturn([
'feature1' => true,
'feature2' => false,
'feature3' => true,
'feature4' => true,
'feature5' => false,
'feature6' => false,
]);
}
}
......@@ -5,6 +5,7 @@ namespace Spec\Minds\Core\Feeds\Elastic;
use Minds\Core\Config;
use Minds\Core\Data\ElasticSearch\Client;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Features\Manager as FeaturesManager;
use Minds\Core\Feeds\Elastic\MetricsSync;
use Minds\Core\Feeds\Elastic\Repository;
use PhpSpec\ObjectBehavior;
......@@ -18,16 +19,20 @@ class RepositorySpec extends ObjectBehavior
/** @var Config */
protected $config;
public function let(Client $client, Config $config)
/** @var FeaturesManager */
protected $features;
public function let(Client $client, Config $config, FeaturesManager $features)
{
$this->client = $client;
$this->config = $config;
$this->features = $features;
$config->get('elasticsearch')
->shouldBeCalled()
->willReturn(['index' => 'minds']);
$this->beConstructedWith($client, $config);
$this->beConstructedWith($client, $config, $features);
}
public function it_is_initializable()
......
......@@ -2,12 +2,26 @@
namespace Spec\Minds\Core\Notification;
use Minds\Core\Features\Manager as FeaturesManager;
use Minds\Core\Notification\Counters;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class CountersSpec extends ObjectBehavior
{
protected $sql;
protected $features;
public function let(
\PDO $sql,
FeaturesManager $features
) {
$this->sql = $sql;
$this->features = $features;
$this->beConstructedWith($sql, $features);
}
public function it_is_initializable()
{
$this->shouldHaveType(Counters::class);
......
......@@ -36,7 +36,9 @@
"psr/http-message": "1.0.1",
"psr/http-server-handler": "1.0.1",
"psr/http-server-middleware": "1.0.1",
"monolog/monolog": "1.25.2"
"monolog/monolog": "1.25.2",
"psr/simple-cache": "1.0.1",
"minds/unleash-client-php": "0.1.1"
},
"repositories": [
{
......@@ -54,6 +56,10 @@
{
"type": "vcs",
"url": "https://github.com/davegardnerisme/cruftflake.git"
},
{
"type": "vcs",
"url": "https://gitlab.com/minds/unleash-client-php.git"
}
],
"autoload": {
......
This diff is collapsed.
......@@ -618,3 +618,11 @@ $CONFIG->set('email_confirmation', [
'signing_key' => '',
'expiration' => 172800, // 48 hours
]);
$CONFIG->set('unleash', [
'apiUrl' => '',
'instanceId' => '',
'applicationName' => '',
'pollingIntervalSeconds' => 300,
'metricsIntervalSeconds' => 15
]);