EV3-431 Profitcenter - betűkód phase1 addd feature flag with laravel pennant

This commit is contained in:
E98Developer 2026-08-08 16:09:54 +02:00
parent af6c02ad2e
commit d6c6f0af1c
21 changed files with 769 additions and 1 deletions

11
.claude/launch.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "t2t-dev",
"runtimeExecutable": "php",
"runtimeArgs": ["artisan", "serve", "--port=8123"],
"port": 8123
}
]
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Filament\Resources\FeatureFlags;
use App\Filament\Resources\FeatureFlags\Pages\CreateFeatureFlag;
use App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag;
use App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags;
use App\Filament\Resources\FeatureFlags\Schemas\FeatureFlagForm;
use App\Filament\Resources\FeatureFlags\Tables\FeatureFlagsTable;
use App\Models\FeatureFlag;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
class FeatureFlagResource extends Resource
{
protected static ?string $model = FeatureFlag::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-flag';
protected static string|\UnitEnum|null $navigationGroup = 'Rendszer';
protected static ?string $navigationLabel = 'Feature flagek';
public static function form(Schema $schema): Schema
{
return FeatureFlagForm::configure($schema);
}
public static function table(Table $table): Table
{
return FeatureFlagsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListFeatureFlags::route('/'),
'create' => CreateFeatureFlag::route('/create'),
'edit' => EditFeatureFlag::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Pages;
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use Filament\Resources\Pages\CreateRecord;
class CreateFeatureFlag extends CreateRecord
{
protected static string $resource = FeatureFlagResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Pages;
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditFeatureFlag extends EditRecord
{
protected static string $resource = FeatureFlagResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Pages;
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListFeatureFlags extends ListRecords
{
protected static string $resource = FeatureFlagResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Schemas;
use App\Models\Role;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class FeatureFlagForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('Alapadatok')
->columnSpanFull()
->schema([
TextInput::make('name')
->label('Kulcs')
->helperText('Ezt a kulcsot kell használni a kódban: Feature::active(\'kulcs\')')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
TextInput::make('label')
->label('Megnevezés')
->required()
->maxLength(255),
Textarea::make('description')
->label('Leírás')
->columnSpanFull(),
Toggle::make('enabled')
->label('Bekapcsolva (rendszer szintű kapcsoló)')
->default(true),
]),
Section::make('Célzás')
->columnSpanFull()
->schema([
TagsInput::make('stages')
->label('Stage-ek')
->helperText('Üresen hagyva minden környezeten aktív. Ismert értékek: DEV, TEST, PROD, d2dtst, e2etst.')
->placeholder('pl. TEST'),
Select::make('roles')
->label('Szerepkörök')
->helperText('Üresen hagyva minden szerepkörnek aktív.')
->multiple()
->options(fn () => Role::pluck('display_name', 'name')->filter()->all()),
]),
]);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Tables;
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\User;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Select;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Table;
use Laravel\Pennant\Feature;
class FeatureFlagsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Kulcs')
->searchable()
->sortable(),
TextColumn::make('label')
->label('Megnevezés')
->searchable(),
ToggleColumn::make('enabled')
->label('Bekapcsolva'),
TextColumn::make('stages')
->label('Stage-ek')
->badge()
->placeholder('mind'),
TextColumn::make('roles')
->label('Szerepkörök')
->badge()
->placeholder('mind'),
TextColumn::make('updated_at')
->label('Módosítva')
->dateTime()
->sortable(),
])
->recordActions([
EditAction::make(),
Action::make('userOverride')
->label('Egyéni felülbírálás')
->icon('heroicon-o-user')
->form([
Select::make('user_id')
->label('Felhasználó')
->searchable()
->options(fn () => User::pluck('name', 'id'))
->required(),
Radio::make('override')
->label('Állapot')
->options([
'inherit' => 'Örökölt (alapértelmezett szabály érvényes)',
'on' => 'Bekapcsolva',
'off' => 'Kikapcsolva',
])
->default('inherit')
->required(),
])
->action(function (array $data, FeatureFlag $record) {
$user = User::findOrFail($data['user_id']);
if ($data['override'] === 'inherit') {
FeatureFlagOverride::where('feature_flag_id', $record->id)
->where('user_id', $user->id)
->delete();
} else {
FeatureFlagOverride::updateOrCreate(
['feature_flag_id' => $record->id, 'user_id' => $user->id],
['active' => $data['override'] === 'on'],
);
}
// csak ennek az egy usernek a gyorsítótárazott Pennant-feloldását
// érvénytelenítjük, a többi felhasználót nem érinti
Feature::for($user)->forget($record->name);
}),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
class FeatureFlag extends BaseAuditable
{
protected $casts = [
'enabled' => 'boolean',
'stages' => 'array',
'roles' => 'array',
];
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class FeatureFlagOverride extends BaseAuditable
{
protected $casts = [
'active' => 'boolean',
];
public function featureFlag(): BelongsTo
{
return $this->belongsTo(FeatureFlag::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Observers;
use App\Models\FeatureFlag;
use Laravel\Pennant\Feature;
class FeatureFlagObserver
{
/**
* A Pennant lazy módon, scope-onként gyorsítótárazza a feloldott értékeket -
* mentés/törlés után purge-elni kell, hogy a módosítás azonnal érvényesüljön.
*/
public function saved(FeatureFlag $flag): void
{
Feature::purge($flag->name);
}
public function deleted(FeatureFlag $flag): void
{
Feature::purge($flag->name);
}
}

View File

@ -2,6 +2,8 @@
namespace App\Providers; namespace App\Providers;
use App\Models\FeatureFlag;
use App\Observers\FeatureFlagObserver;
use App\Repositories\AddressRepository; use App\Repositories\AddressRepository;
use App\Repositories\AddressRepositoryInterface; use App\Repositories\AddressRepositoryInterface;
use App\Repositories\ContactRepository; use App\Repositories\ContactRepository;
@ -20,6 +22,8 @@
use App\Repositories\ProfitCenterRepositoryInterface; use App\Repositories\ProfitCenterRepositoryInterface;
use App\Repositories\SupplierRepository; use App\Repositories\SupplierRepository;
use App\Repositories\SupplierRepositoryInterface; use App\Repositories\SupplierRepositoryInterface;
use App\Services\FeatureFlagRegistrar;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -53,6 +57,14 @@ public function boot(): void
{ {
URL::forceScheme('https'); URL::forceScheme('https');
FeatureFlag::observe(FeatureFlagObserver::class);
// Schema::hasTable védi a friss telepítést / migrate-et megelőző state-et,
// amikor a feature_flags tábla még nem létezik.
if (Schema::hasTable('feature_flags')) {
app(FeatureFlagRegistrar::class)->registerAll();
}
\Illuminate\Database\Schema\Blueprint::macro('userIdFields', function () { \Illuminate\Database\Schema\Blueprint::macro('userIdFields', function () {
$this->unsignedInteger('created_by')->nullable(); $this->unsignedInteger('created_by')->nullable();
$this->unsignedInteger('updated_by')->nullable(); $this->unsignedInteger('updated_by')->nullable();
@ -88,6 +100,10 @@ public function boot(): void
\Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.create-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\CreateProfitCenterSupplierSchedule::class); \Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.create-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\CreateProfitCenterSupplierSchedule::class);
\Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.edit-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\EditProfitCenterSupplierSchedule::class); \Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.edit-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\EditProfitCenterSupplierSchedule::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.list-feature-flags', \App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.create-feature-flag', \App\Filament\Resources\FeatureFlags\Pages\CreateFeatureFlag::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.edit-feature-flag', \App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag::class);
\Livewire\Livewire::component('filament.livewire.notifications', \Filament\Livewire\Notifications::class); \Livewire\Livewire::component('filament.livewire.notifications', \Filament\Livewire\Notifications::class);
\Livewire\Livewire::component('filament.livewire.database-notifications', \Filament\Livewire\DatabaseNotifications::class); \Livewire\Livewire::component('filament.livewire.database-notifications', \Filament\Livewire\DatabaseNotifications::class);
\Livewire\Livewire::component('filament.livewire.sidebar', \Filament\Livewire\Sidebar::class); \Livewire\Livewire::component('filament.livewire.sidebar', \Filament\Livewire\Sidebar::class);

View File

@ -0,0 +1,53 @@
<?php
namespace App\Services;
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\User;
use Laravel\Pennant\Feature;
class FeatureFlagRegistrar
{
/**
* Regisztrálja az összes adatbázisban tárolt feature flaget a Pennant-ban.
*
* Az egyéni felhasználói felülbírálásokat szándékosan NEM a Pennant saját
* tárolóján (Feature::for($user)->activate()) keresztül kezeljük, mert a
* Pennant purge() minden scope-ra töröl egy adott flag névre - egy admin
* mentés (pl. enabled kikapcsolása) így törölné az explicit felülbírálásokat
* is. Ezért az override-okat saját táblában tároljuk, a resolver pedig ezt
* nézi meg elsőként; a Pennant tárolója így csak a szabály-alapú
* alapértelmezett feloldást cache-eli, amit bármikor biztonságos purge-elni.
*/
public function registerAll(): void
{
FeatureFlag::all()->each(function (FeatureFlag $flag) {
Feature::define($flag->name, function (?User $user) use ($flag) {
if ($user) {
$override = FeatureFlagOverride::where('feature_flag_id', $flag->id)
->where('user_id', $user->id)
->first();
if ($override) {
return $override->active;
}
}
if (! $flag->enabled) {
return false;
}
if ($flag->stages && ! in_array(config('app.stage'), $flag->stages, true)) {
return false;
}
if ($flag->roles && (! $user || ! $user->hasRole($flag->roles))) {
return false;
}
return true;
});
});
}
}

View File

@ -18,6 +18,7 @@
"laravel-json-api/laravel": "^5.1", "laravel-json-api/laravel": "^5.1",
"laravel-lang/lang": "^15.0", "laravel-lang/lang": "^15.0",
"laravel/framework": "^12.48", "laravel/framework": "^12.48",
"laravel/pennant": "^1.24",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",

79
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "05f09a61a6722503ee896c57c530bf6b", "content-hash": "e6b0d54f37a81c8c859c977ad5d3527c",
"packages": [ "packages": [
{ {
"name": "ahmedhakeem/extra", "name": "ahmedhakeem/extra",
@ -5084,6 +5084,83 @@
}, },
"time": "2026-03-26T14:51:54+00:00" "time": "2026-03-26T14:51:54+00:00"
}, },
{
"name": "laravel/pennant",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pennant.git",
"reference": "b99fbc8038eb2c0a2642e9e13077a75e892b61d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pennant/zipball/b99fbc8038eb2c0a2642e9e13077a75e892b61d9",
"reference": "b99fbc8038eb2c0a2642e9e13077a75e892b61d9",
"shasum": ""
},
"require": {
"illuminate/console": "^10.0|^11.0|^12.0|^13.0",
"illuminate/container": "^10.0|^11.0|^12.0|^13.0",
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"illuminate/database": "^10.0|^11.0|^12.0|^13.0",
"illuminate/queue": "^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/finder": "^6.0|^7.0|^8.0"
},
"require-dev": {
"laravel/octane": "^1.4|^2.0",
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Feature": "Laravel\\Pennant\\Feature"
},
"providers": [
"Laravel\\Pennant\\PennantServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php",
"src/functions.php"
],
"psr-4": {
"Laravel\\Pennant\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "A simple, lightweight library for managing feature flags.",
"homepage": "https://github.com/laravel/pennant",
"keywords": [
"feature",
"flags",
"laravel",
"pennant"
],
"support": {
"issues": "https://github.com/laravel/pennant/issues",
"source": "https://github.com/laravel/pennant"
},
"time": "2026-06-28T17:14:13+00:00"
},
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
"version": "v0.3.16", "version": "v0.3.16",

44
config/pennant.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Pennant Store
|--------------------------------------------------------------------------
|
| Here you will specify the default store that Pennant should use when
| storing and resolving feature flag values. Pennant ships with the
| ability to store flag values in an in-memory array or database.
|
| Supported: "array", "database"
|
*/
'default' => env('PENNANT_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Pennant Stores
|--------------------------------------------------------------------------
|
| Here you may configure each of the stores that should be available to
| Pennant. These stores shall be used to store resolved feature flag
| values - you may configure as many as your application requires.
|
*/
'stores' => [
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'connection' => null,
'table' => 'features',
],
],
];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Pennant\Migrations\PennantMigration;
return new class extends PennantMigration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('features', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('scope');
$table->text('value');
$table->timestamps();
$table->unique(['name', 'scope']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('features');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('feature_flags', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('label');
$table->text('description')->nullable();
$table->boolean('enabled')->default(true);
$table->json('stages')->nullable();
$table->json('roles')->nullable();
// Audit mezők a BaseAuditable-hoz
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->unsignedBigInteger('deleted_by')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('feature_flags');
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('feature_flag_overrides', function (Blueprint $table) {
$table->id();
$table->foreignId('feature_flag_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->boolean('active');
// Audit mezők a BaseAuditable-hoz
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->unsignedBigInteger('deleted_by')->nullable();
$table->timestamps();
$table->unique(['feature_flag_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('feature_flag_overrides');
}
};

View File

@ -20,6 +20,7 @@
['displayName' => 'Munkanaptárak', 'link' => \App\Filament\Resources\WorkCalendars\WorkCalendarResource::getUrl()], ['displayName' => 'Munkanaptárak', 'link' => \App\Filament\Resources\WorkCalendars\WorkCalendarResource::getUrl()],
]], ]],
['name' => 'calendarTest', 'displayName' => 'Naptár Teszt', 'icon' => 'idopontok', 'link' => \App\Filament\Pages\CalendarTest::getUrl(), 'roles' => ['root','developer','admin']], ['name' => 'calendarTest', 'displayName' => 'Naptár Teszt', 'icon' => 'idopontok', 'link' => \App\Filament\Pages\CalendarTest::getUrl(), 'roles' => ['root','developer','admin']],
['name' => 'featureFlags', 'displayName' => 'Feature flagek', 'icon' => 'setup', 'link' => \App\Filament\Resources\FeatureFlags\FeatureFlagResource::getUrl(), 'roles' => ['root','developer']],
]; ];
@endphp @endphp

View File

@ -0,0 +1,30 @@
<?php
use App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags;
use App\Models\FeatureFlag;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
test('a lista oldal hiba nélkül renderel, ha egy flagnek van szerepköre és stage-e', function () {
$adminRole = Role::create(['name' => 'admin', 'display_name' => 'Admin']);
$admin = User::factory()->create();
$admin->addRole($adminRole);
FeatureFlag::create([
'name' => 'ui-test-flag',
'label' => 'UI teszt flag',
'enabled' => true,
'stages' => ['TEST'],
'roles' => ['admin'],
]);
$this->actingAs($admin);
Livewire::test(ListFeatureFlags::class)
->assertSuccessful();
});

View File

@ -0,0 +1,123 @@
<?php
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\Role;
use App\Models\User;
use App\Services\FeatureFlagRegistrar;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Laravel\Pennant\Feature;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function defineFlag(array $attributes): FeatureFlag
{
$flag = FeatureFlag::create(array_merge([
'name' => 'test-flag',
'label' => 'Test flag',
'enabled' => true,
'stages' => null,
'roles' => null,
], $attributes));
app(FeatureFlagRegistrar::class)->registerAll();
return $flag;
}
test('a kikapcsolt flag mindenkinek inaktív', function () {
defineFlag(['enabled' => false]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('a stage-mismatch inaktívvá teszi a flaget', function () {
Config::set('app.stage', 'PROD');
defineFlag(['stages' => ['TEST']]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('a stage-match aktívvá teszi a flaget', function () {
Config::set('app.stage', 'TEST');
defineFlag(['stages' => ['TEST']]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});
test('a role-mismatch inaktívvá teszi a flaget', function () {
Role::create(['name' => 'admin', 'display_name' => 'Admin']);
defineFlag(['roles' => ['admin']]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('a role-match aktívvá teszi a flaget', function () {
$role = Role::create(['name' => 'admin', 'display_name' => 'Admin']);
defineFlag(['roles' => ['admin']]);
$user = User::factory()->create();
$user->addRole($role);
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});
test('a user-szintű explicit felülbírálás felülír mindent', function () {
$flag = defineFlag(['enabled' => false]);
$user = User::factory()->create();
FeatureFlagOverride::create([
'feature_flag_id' => $flag->id,
'user_id' => $user->id,
'active' => true,
]);
Feature::for($user)->forget('test-flag');
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});
test('a flag admin-mentése nem törli az egyéni felülbírálásokat', function () {
$flag = defineFlag(['enabled' => true]);
$user = User::factory()->create();
FeatureFlagOverride::create([
'feature_flag_id' => $flag->id,
'user_id' => $user->id,
'active' => false,
]);
Feature::for($user)->forget('test-flag');
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
// Admin módosítja a flaget (pl. leírás) - ez kiváltja a FeatureFlagObserver purge-öt
$flag->update(['description' => 'módosítva']);
app(FeatureFlagRegistrar::class)->registerAll();
expect(FeatureFlagOverride::where('feature_flag_id', $flag->id)->where('user_id', $user->id)->exists())
->toBeTrue();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('üres stages/roles esetén mindenkinek aktív', function () {
defineFlag([]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});