Skip to content

Commit

Permalink
Merge pull request #4 from codinglabsau/st/sc-7287-move-syncfeaturesa…
Browse files Browse the repository at this point in the history
…ction-over-to-package-with

St/sc 7287 move syncfeaturesaction over to package with
  • Loading branch information
stevethomas authored Aug 30, 2022
2 parents 5153222 + c48931d commit a266f3a
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 49 deletions.
27 changes: 25 additions & 2 deletions config/feature-flags.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

return [

/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Declare features that are managed by the app with the Feature
| Flag package. The format is ['name' => FeatureState::on()].
*/

'features' => [],

/*
|--------------------------------------------------------------------------
| Always On
|--------------------------------------------------------------------------
|
| Declare the environments where features will be synced to the on
| state. This is useful if features should always be on locally.
| Note this only impacts the behaviour of the sync action.
*/

'always_on' => [],

/*
|--------------------------------------------------------------------------
| Cache
Expand All @@ -20,8 +43,8 @@
| Models
|--------------------------------------------------------------------------
|
| If you need to customise any models used then you can swap them out by
| replacing the default models defined here.
| If you need to customise any models used then you can swap
| them out by replacing the default models defined here.
*/

'feature_model' => \Codinglabs\FeatureFlags\Models\Feature::class,
Expand Down
28 changes: 28 additions & 0 deletions src/Actions/SyncFeaturesAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Codinglabs\FeatureFlags\Actions;

use Codinglabs\FeatureFlags\Models\Feature;
use Codinglabs\FeatureFlags\Enums\FeatureState;

class SyncFeaturesAction
{
public function __invoke(): void
{
$features = collect(config('feature-flags.features'))
->map(fn ($state, $name) => [
'name' => $name,
'state' => app()->environment(config('feature-flags.always_on', []))
? FeatureState::on()
: $state
]);

$featureModels = Feature::all();

$featureModels->whereNotIn('name', $features->pluck('name'))
->each(fn (Feature $feature) => $feature->delete());

$features->whereNotIn('name', $featureModels->pluck('name'))
->each(fn (array $feature) => Feature::create($feature));
}
}
4 changes: 2 additions & 2 deletions src/Casts/FeatureStateCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

class FeatureStateCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
public function get($model, string $key, $value, array $attributes): FeatureState
{
return FeatureState::from($attributes['state']);
}

public function set($model, string $key, $value, array $attributes)
public function set($model, string $key, $value, array $attributes): array
{
if (! $value instanceof FeatureState) {
throw new InvalidArgumentException('The given value is not an instance of FeatureState.');
Expand Down
3 changes: 2 additions & 1 deletion src/Models/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace Codinglabs\FeatureFlags\Models;

use Illuminate\Database\Eloquent\Model;
use Codinglabs\FeatureFlags\Casts\FeatureStateCast;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Feature extends \Illuminate\Database\Eloquent\Model
class Feature extends Model
{
use HasFactory;

Expand Down
43 changes: 43 additions & 0 deletions tests/BladeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use Codinglabs\FeatureFlags\Models\Feature;
use Codinglabs\FeatureFlags\Enums\FeatureState;
use Codinglabs\FeatureFlags\Facades\FeatureFlag;
use Illuminate\Foundation\Testing\Concerns\InteractsWithViews;

uses(InteractsWithViews::class);

beforeEach(function () {
config([
'feature-flags.cache_store' => 'array',
'feature-flags.cache_prefix' => 'testing',
]);

cache()->store('array')->clear();
});

afterEach(function () {
FeatureFlag::reset();
});

it('does not reveal things when feature is off', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::off()
]);

$view = $this->blade("@feature('some-feature') secret things @endfeature");

$view->assertDontSee('secret things');
});

it('reveals things when feature is on ', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::on()
]);

$view = $this->blade("@feature('some-feature') secret things @endfeature");

$view->assertSee('secret things');
});
95 changes: 51 additions & 44 deletions tests/FeaturesTest.php → tests/FeatureFlagTest.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<?php

use Illuminate\Support\Facades\Route;
use Codinglabs\FeatureFlags\Models\Feature;
use Codinglabs\FeatureFlags\Enums\FeatureState;
use Codinglabs\FeatureFlags\Facades\FeatureFlag;
use Codinglabs\FeatureFlags\Middleware\VerifyFeatureIsOn;
use Codinglabs\FeatureFlags\Events\FeatureUpdatedEvent;
use Codinglabs\FeatureFlags\Exceptions\MissingFeatureException;

beforeEach(function () {
Expand All @@ -13,17 +12,22 @@
'feature-flags.cache_prefix' => 'testing',
]);

Route::get('test-middleware', function () {
return 'ok';
})->middleware(VerifyFeatureIsOn::class . ':some-feature');

cache()->store('array')->clear();
});

afterEach(function () {
FeatureFlag::reset();
});

it('throws an exception if casting to a feature state that does not exist', function () {
$this->expectException(\InvalidArgumentException::class);

Feature::factory()->create([
'name' => 'some-feature',
'state' => 'foo',
]);
});

it('throws an exception if calling isOn on a feature that does not exist', function () {
$this->expectException(MissingFeatureException::class);

Expand Down Expand Up @@ -146,7 +150,7 @@
->and(FeatureFlag::getState('some-on-feature'))->toBe(FeatureState::on());
});

it('can update a features state', function () {
it('can turn on a feature', function () {
Event::fake();

Feature::factory()->create([
Expand All @@ -156,69 +160,72 @@

cache()->store('array')->set('testing.some-feature', 'off');

FeatureFlag::updateFeatureState('some-feature', FeatureState::on());
FeatureFlag::turnOn('some-feature');

Event::assertDispatched(\Codinglabs\FeatureFlags\Events\FeatureUpdatedEvent::class);
Event::assertDispatched(FeatureUpdatedEvent::class);
expect(FeatureFlag::isOn('some-feature'))->toBeTrue()
->and(FeatureFlag::isOff('some-feature'))->toBeFalse()
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::on()->value);
});

it('uses the default cache store when cache store has not been set', function () {
config(['cache.default' => 'file']);

config(['feature-flags.cache_store' => env('FEATURES_CACHE_STORE', config('cache.default'))]);
it('can turn off a feature', function () {
Event::fake();

expect(config('feature-flags.cache_store'))->toBe('file');
});
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::on()
]);

it('returns a 500 status when a feature does not exist', function () {
$this->withoutExceptionHandling();
cache()->store('array')->set('testing.some-feature', 'on');

$this->expectException(MissingFeatureException::class);
FeatureFlag::turnOff('some-feature');

$this->get('test-middleware')
->assertStatus(500);
Event::assertDispatched(FeatureUpdatedEvent::class);
expect(FeatureFlag::isOn('some-feature'))->toBeFalse()
->and(FeatureFlag::isOff('some-feature'))->toBeTrue()
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::off()->value);
});

it('returns a 404 status when a feature is off', function () {
it('can make a feature dynamic', function () {
Event::fake();

Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::off()
'state' => FeatureState::on()
]);

$this->get('test-middleware')
->assertStatus(404);
});
cache()->store('array')->set('testing.some-feature', 'on');

it('returns a 404 status when a feature is dynamic', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::dynamic()
]);
FeatureFlag::makeDynamic('some-feature');

$this->get('test-middleware')
->assertStatus(404);
Event::assertDispatched(FeatureUpdatedEvent::class);
expect(FeatureFlag::isOn('some-feature'))->toBeFalse()
->and(FeatureFlag::isOff('some-feature'))->toBeTrue()
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::dynamic()->value);
});

it('returns an ok status when a feature is dynamic and enabled', function () {
it('can update a features state', function () {
Event::fake();

Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::dynamic()
'state' => FeatureState::off()
]);

FeatureFlag::registerDynamicHandler('some-feature', fn ($feature) => true);
cache()->store('array')->set('testing.some-feature', 'off');

$this->get('test-middleware')
->assertOk();
FeatureFlag::updateFeatureState('some-feature', FeatureState::on());

Event::assertDispatched(FeatureUpdatedEvent::class);
expect(FeatureFlag::isOn('some-feature'))->toBeTrue()
->and(FeatureFlag::isOff('some-feature'))->toBeFalse()
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::on()->value);
});

it('returns an ok status when a feature is on', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::on()
]);
it('uses the default cache store when cache store has not been set', function () {
config(['cache.default' => 'file']);

$this->get('test-middleware')
->assertOk();
config(['feature-flags.cache_store' => env('FEATURES_CACHE_STORE', config('cache.default'))]);

expect(config('feature-flags.cache_store'))->toBe('file');
});
76 changes: 76 additions & 0 deletions tests/MiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

use Illuminate\Support\Facades\Route;
use Codinglabs\FeatureFlags\Models\Feature;
use Codinglabs\FeatureFlags\Enums\FeatureState;
use Codinglabs\FeatureFlags\Facades\FeatureFlag;
use Codinglabs\FeatureFlags\Middleware\VerifyFeatureIsOn;
use Codinglabs\FeatureFlags\Exceptions\MissingFeatureException;

beforeEach(function () {
config([
'feature-flags.cache_store' => 'array',
'feature-flags.cache_prefix' => 'testing',
]);

Route::get('test-middleware', function () {
return 'ok';
})->middleware(VerifyFeatureIsOn::class . ':some-feature');

cache()->store('array')->clear();
});

afterEach(function () {
FeatureFlag::reset();
});

it('returns a 500 status when a feature does not exist', function () {
$this->withoutExceptionHandling();

$this->expectException(MissingFeatureException::class);

$this->get('test-middleware')
->assertStatus(500);
});

it('returns a 404 status when a feature is off', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::off()
]);

$this->get('test-middleware')
->assertStatus(404);
});

it('returns a 404 status when a feature is dynamic', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::dynamic()
]);

$this->get('test-middleware')
->assertStatus(404);
});

it('returns an ok status when a feature is dynamic and enabled', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::dynamic()
]);

FeatureFlag::registerDynamicHandler('some-feature', fn ($feature) => true);

$this->get('test-middleware')
->assertOk();
});

it('returns an ok status when a feature is on', function () {
Feature::factory()->create([
'name' => 'some-feature',
'state' => FeatureState::on()
]);

$this->get('test-middleware')
->assertOk();
});
Loading

0 comments on commit a266f3a

Please sign in to comment.