diff --git a/database/factories/SiteFactory.php b/database/factories/SiteFactory.php new file mode 100644 index 0000000..c21f485 --- /dev/null +++ b/database/factories/SiteFactory.php @@ -0,0 +1,22 @@ + */ +class SiteFactory extends Factory +{ + protected $model = Site::class; + + /** @return array */ + public function definition(): array + { + return [ + 'name' => fake()->word(), + 'slug' => fake()->word(), + 'domain' => fake()->domainName(), + ]; + } +} diff --git a/database/migrations/create_menus_table.php.stub b/database/migrations/create_menus_table.php.stub index 652535e..7c963ba 100644 --- a/database/migrations/create_menus_table.php.stub +++ b/database/migrations/create_menus_table.php.stub @@ -11,6 +11,7 @@ return new class extends Migration Schema::create('menus', function (Blueprint $table) { $table->id(); $table->string('name'); + $table->foreignId('site_id')->constrained()->cascadeOnDelete(); $table->json('meta')->nullable(); $table->boolean('is_visible')->default(true); $table->timestamps(); diff --git a/database/migrations/create_pages_table.php.stub b/database/migrations/create_pages_table.php.stub index 24406d5..e3423e6 100644 --- a/database/migrations/create_pages_table.php.stub +++ b/database/migrations/create_pages_table.php.stub @@ -10,11 +10,12 @@ return new class extends Migration { Schema::create('pages', function (Blueprint $table) { $table->id(); + $table->foreignId('site_id')->constrained()->cascadeOnDelete(); $table->foreignId('parent_id')->nullable()->references('id')->on($table->getTable())->nullOnDelete(); $table->string('type')->default('page'); $table->string('title'); $table->string('slug'); - $table->string('computed_slug')->unique(); + $table->string('computed_slug'); $table->json('blocks')->nullable(); $table->string('layout')->nullable(); $table->timestamp('published_at')->nullable(); @@ -24,11 +25,11 @@ return new class extends Migration $table->timestamps(); $table->softDeletes(); - // Indexes for tree queries and ordering $table->index('parent_id'); $table->index('order'); $table->index(['parent_id', 'order']); // Composite index for efficient sibling ordering - $table->index('type'); // For filtering by page type + $table->index('type'); + $table->unique(['site_id', 'computed_slug']); }); } diff --git a/database/migrations/create_sites_table.php.stub b/database/migrations/create_sites_table.php.stub new file mode 100644 index 0000000..2e8ebdc --- /dev/null +++ b/database/migrations/create_sites_table.php.stub @@ -0,0 +1,34 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->string('domain')->nullable()->unique(); + $table->json('meta')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + Schema::create('site_user', function (Blueprint $table) { + $table->id(); + $table->foreignId('site_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + $table->unique(['site_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('site_user'); + Schema::dropIfExists('sites'); + } +}; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9e9c337..21c89c9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -60,12 +60,30 @@ parameters: count: 1 path: database/factories/PageFactory.php + - + message: '#^Return type \(array\\) of method Siteman\\Cms\\Database\\Factories\\SiteFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/SiteFactory.php + - message: '#^PHPDoc tag @var with type Filament\\Forms\\Components\\Field is not subtype of native type \$this\(Siteman\\Cms\\CmsServiceProvider\)\.$#' identifier: varTag.nativeType count: 1 path: src/CmsServiceProvider.php + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$site_id\.$#' + identifier: property.notFound + count: 1 + path: src/Models/Menu.php + + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$site_id\.$#' + identifier: property.notFound + count: 1 + path: src/Models/Page.php + - message: '#^Method Siteman\\Cms\\Models\\Page\:\:getChildren\(\) should return Illuminate\\Support\\Collection\ but returns Staudenmeir\\LaravelAdjacencyList\\Eloquent\\Collection\\.$#' identifier: return.type @@ -84,6 +102,12 @@ parameters: count: 1 path: src/Models/Page.php + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$site_id\.$#' + identifier: property.notFound + count: 1 + path: src/Models/Role.php + - message: '#^Call to an undefined method Filament\\Forms\\Components\\Textarea\:\:asPageMetaField\(\)\.$#' identifier: method.notFound diff --git a/src/CmsServiceProvider.php b/src/CmsServiceProvider.php index 313a080..a133d04 100644 --- a/src/CmsServiceProvider.php +++ b/src/CmsServiceProvider.php @@ -23,6 +23,7 @@ use Siteman\Cms\Commands\InstallCommand; use Siteman\Cms\Commands\MakeBlockCommand; use Siteman\Cms\Commands\MakeSettingsCommand; +use Siteman\Cms\Commands\MakeSiteCommand; use Siteman\Cms\Commands\MakeThemeCommand; use Siteman\Cms\Commands\PublishCommand; use Siteman\Cms\Commands\UpdateCommand; @@ -66,6 +67,7 @@ public function configurePackage(Package $package): void ->hasViews('siteman') ->hasRoute('web') ->hasMigrations([ + 'create_sites_table', 'create_menus_table', 'create_pages_table', '../settings/create_general_settings', @@ -78,6 +80,7 @@ public function configurePackage(Package $package): void MakeThemeCommand::class, MakeBlockCommand::class, MakeSettingsCommand::class, + MakeSiteCommand::class, CreateAdminCommand::class, ]); @@ -110,6 +113,9 @@ public function bootingPackage(): void )); $config->set('tags.tag_model', Tag::class); + $config->set('permission.teams', true); + $config->set('permission.models.role', Models\Role::class); + $config->set('permission.column_names.team_foreign_key', 'site_id'); $config->set('filament-shield.shield_resource.show_model_path', false); $config->set('filament-shield.permission_prefixes.resource', [ 'view_any', diff --git a/src/Commands/CreateAdminCommand.php b/src/Commands/CreateAdminCommand.php index 4ed201e..8e298db 100644 --- a/src/Commands/CreateAdminCommand.php +++ b/src/Commands/CreateAdminCommand.php @@ -4,18 +4,30 @@ use Illuminate\Console\Command; use Siteman\Cms\Facades\Siteman; +use Siteman\Cms\Models\Site; use function Laravel\Prompts\password; +use function Laravel\Prompts\select; use function Laravel\Prompts\text; class CreateAdminCommand extends Command { - public $signature = 'siteman:create-admin username {name?} {email?} {password?}'; + public $signature = 'siteman:create-admin {name?} {email?} {password?} {--site= : Site ID or slug}'; - public $description = 'This command creates a Siteman admin'; + public $description = 'This command creates a Siteman admin for a site'; public function handle(): int { + $site = $this->resolveSite(); + + if (!$site) { + $this->components->error('No site found. Please create a site first or specify a valid --site option.'); + + return self::FAILURE; + } + + Siteman::setCurrentSite($site); + $username = $this->argument('name') ?? text('Enter the admin user name', default: 'admin'); $email = $this->argument('email') ?? text('Enter the admin email', default: 'admin@admin.com'); $password = $this->argument('password') ?? password('Enter the admin password'); @@ -26,10 +38,40 @@ public function handle(): int 'password' => bcrypt($password), ]); + $user->sites()->attach($site); $user->assignRole(Siteman::createSuperAdminRole()); - $this->components->info('User successfully created. You can now log in at '.Siteman::getLoginUrl()); + $this->components->info("User successfully created for site '{$site->name}'. You can now log in at ".Siteman::getLoginUrl()); return self::SUCCESS; } + + protected function resolveSite(): ?Site + { + $siteOption = $this->option('site'); + + if ($siteOption) { + return Site::where('id', $siteOption) + ->orWhere('slug', $siteOption) + ->orWhere('domain', $siteOption) + ->first(); + } + + $sites = Site::all(); + + if ($sites->isEmpty()) { + return null; + } + + if ($sites->count() === 1) { + return $sites->first(); + } + + $siteId = select( + label: 'Select a site for the admin user', + options: $sites->pluck('name', 'id')->toArray(), + ); + + return Site::find($siteId); + } } diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index 1cae777..2e31913 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -1,4 +1,4 @@ -createDefaultSite(); + if ($userModel::count() > 0 && !confirm('There are already users in the database. Do you want to continue?')) { $this->components->info('No user created. All done.'); @@ -80,7 +83,15 @@ public function handle(Filesystem $filesystem): int $email = text('Enter the admin email', default: 'admin@admin.com'); $password = password('Enter the admin password'); - Process::run([(new PhpExecutableFinder)->find(), 'artisan', 'siteman:create-admin', $name, $email, $password], function ($type, $line) { + Process::run([ + (new PhpExecutableFinder)->find(), + 'artisan', + 'siteman:create-admin', + $name, + $email, + $password, + '--site='.$site->id, + ], function ($type, $line) { $this->output->write($line); }); @@ -94,4 +105,23 @@ public function handle(Filesystem $filesystem): int return self::SUCCESS; } + + protected function createDefaultSite(): Site + { + if ($site = Site::first()) { + $this->components->info("Using existing site: {$site->name}"); + + return $site; + } + + $siteName = text('Enter the site name', default: config('app.name', 'My Site')); + $siteDomain = text('Enter the site domain (optional)', default: '', hint: 'e.g., example.com'); + + $this->call('siteman:make-site', [ + 'name' => $siteName, + '--domain' => $siteDomain ?: null, + ]); + + return Site::latest()->first(); + } } diff --git a/src/Commands/MakeSiteCommand.php b/src/Commands/MakeSiteCommand.php new file mode 100644 index 0000000..961d588 --- /dev/null +++ b/src/Commands/MakeSiteCommand.php @@ -0,0 +1,50 @@ +argument('name') ?? text( + label: 'Enter the site name', + default: config('app.name', 'My Site'), + required: true, + ); + + $slug = $this->option('slug') ?? Str::slug($name); + $domain = $this->option('domain') ?: null; + + if (Site::where('slug', $slug)->exists()) { + $this->components->error("A site with slug '{$slug}' already exists."); + + return self::FAILURE; + } + + if ($domain && Site::where('domain', $domain)->exists()) { + $this->components->error("A site with domain '{$domain}' already exists."); + + return self::FAILURE; + } + + $site = Site::create([ + 'name' => $name, + 'slug' => $slug, + 'domain' => $domain, + ]); + + $this->components->info("Site '{$site->name}' created successfully with slug '{$site->slug}'."); + + return self::SUCCESS; + } +} diff --git a/src/Http/Middleware/SitePermissionMiddleware.php b/src/Http/Middleware/SitePermissionMiddleware.php new file mode 100644 index 0000000..f72dcd8 --- /dev/null +++ b/src/Http/Middleware/SitePermissionMiddleware.php @@ -0,0 +1,18 @@ +user())) { + setPermissionsTeamId(Filament::getTenant()->getKey()); + } + + return $next($request); + } +} diff --git a/src/Http/SitemanController.php b/src/Http/SitemanController.php index 45ba720..3078e68 100644 --- a/src/Http/SitemanController.php +++ b/src/Http/SitemanController.php @@ -1,4 +1,4 @@ -where('computed_slug', '/'.ltrim($request->path(), '/'))->first(); if (!$page) { $rootPath = '/'.str($request->path())->before('/'); diff --git a/src/Models/Concerns/HasSite.php b/src/Models/Concerns/HasSite.php new file mode 100644 index 0000000..3282d21 --- /dev/null +++ b/src/Models/Concerns/HasSite.php @@ -0,0 +1,37 @@ +site_id) && $site = Siteman::getCurrentSite()) { + $model->site_id = $site->id; + } + }); + + static::addGlobalScope('site', function (Builder $query) { + if ($site = Siteman::getCurrentSite()) { + $table = $query->getModel()->getTable(); + $query->where("{$table}.site_id", $site->id); + } + }); + } + + public function site(): BelongsTo + { + return $this->belongsTo(Site::class); + } +} diff --git a/src/Models/Menu.php b/src/Models/Menu.php index 60aff9f..a911ebc 100644 --- a/src/Models/Menu.php +++ b/src/Models/Menu.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Carbon; use Siteman\Cms\Database\Factories\MenuFactory; +use Siteman\Cms\Models\Concerns\HasSite; /** * @property int $id @@ -24,6 +25,7 @@ class Menu extends Model { use HasFactory; + use HasSite; protected static string $factory = MenuFactory::class; diff --git a/src/Models/Page.php b/src/Models/Page.php index 74b48d6..dd5b34d 100644 --- a/src/Models/Page.php +++ b/src/Models/Page.php @@ -17,6 +17,7 @@ use RalphJSmit\Laravel\SEO\Support\SEOData; use Siteman\Cms\Contracts\MenuItemInterface; use Siteman\Cms\Database\Factories\PageFactory; +use Siteman\Cms\Models\Concerns\HasSite; use Spatie\Feed\Feedable; use Spatie\Feed\FeedItem; use Spatie\MediaLibrary\HasMedia; @@ -45,6 +46,7 @@ class Page extends Model implements Feedable, HasMedia, MenuItemInterface use HasFactory; use HasRecursiveRelationships; use HasSEO; + use HasSite; use HasTags; use InteractsWithMedia; use SoftDeletes; diff --git a/src/Models/Role.php b/src/Models/Role.php new file mode 100644 index 0000000..09846e8 --- /dev/null +++ b/src/Models/Role.php @@ -0,0 +1,11 @@ +belongsToMany( + config('siteman.models.user'), + 'site_user' + )->withTimestamps(); + } + + public function pages(): HasMany + { + return $this->hasMany(Page::class); + } + + public function menus(): HasMany + { + return $this->hasMany(Menu::class); + } +} diff --git a/src/Pages/EditSite.php b/src/Pages/EditSite.php new file mode 100644 index 0000000..058a972 --- /dev/null +++ b/src/Pages/EditSite.php @@ -0,0 +1,13 @@ +whereHas('sites', fn (Builder $q) => $q->where('sites.id', $site->id)); + } + + return $query; + } + public static function form(Schema $schema): Schema { return $schema diff --git a/src/Siteman.php b/src/Siteman.php index 335145d..e7a8ef5 100644 --- a/src/Siteman.php +++ b/src/Siteman.php @@ -6,6 +6,7 @@ use Filament\Facades\Filament; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Context; use Illuminate\Support\Str; use RuntimeException; use Siteman\Cms\Blocks\BlockRegistry; @@ -13,6 +14,7 @@ use Siteman\Cms\Blocks\MarkdownBlock; use Siteman\Cms\Enums\FormHook; use Siteman\Cms\Models\Menu; +use Siteman\Cms\Models\Site; use Siteman\Cms\PageTypes\BlogIndex; use Siteman\Cms\PageTypes\Page; use Siteman\Cms\PageTypes\RssFeed; @@ -157,7 +159,7 @@ public function createSuperAdminRole(): Role public function createRole(string $name): Role { - return $this->getRoleModel()::create([ + return $this->getRoleModel()::firstOrCreate([ 'name' => $name, 'guard_name' => 'web', ]); @@ -207,6 +209,36 @@ public function getLoginUrl(): string return route('filament.admin.auth.login'); } + public function getCurrentSite(): ?Site + { + if ($site = Context::get('current_site')) { + return $site; + } + $site = Filament::getTenant(); + if ($site instanceof Site) { + $this->setCurrentSite($site); + + return $site; + } + if ($site = Site::where('domain', request()->getHost())->first()) { + $this->setCurrentSite($site); + + return $site; + } + + return null; + } + + public function setCurrentSite(Site|int $site): void + { + if (is_int($site)) { + $site = Site::findOrFail($site); + } + Context::add('current_site', $site); + Filament::setTenant($site, true); + app(PermissionRegistrar::class)->setPermissionsTeamId($site->id); + } + protected function getDefaultPermissionIdentifier(string $resource): string { return Str::of($resource) diff --git a/src/SitemanPlugin.php b/src/SitemanPlugin.php index 3b2a0cf..efbc7bc 100644 --- a/src/SitemanPlugin.php +++ b/src/SitemanPlugin.php @@ -8,7 +8,10 @@ use Filament\Panel; use Filament\Support\Colors\Color; use Filament\Support\Enums\Width; +use Siteman\Cms\Http\Middleware\SitePermissionMiddleware; use Siteman\Cms\Models\Page; +use Siteman\Cms\Models\Site; +use Siteman\Cms\Pages\EditSite; use Siteman\Cms\Pages\SettingsPage; use Siteman\Cms\Pages\SiteHealthPage; use Siteman\Cms\Resources\Menus\MenuResource; @@ -32,6 +35,12 @@ public function getId(): string public function register(Panel $panel): void { + $panel + ->tenant(Site::class, 'slug', 'site') + ->tenantMiddleware([SitePermissionMiddleware::class], true) + ->tenantProfile(EditSite::class) + ->tenantMenu(); + $panel->navigationGroups([ NavigationGroup::make('Content')->collapsible(false), NavigationGroup::make('Admin')->collapsible()->collapsed(), diff --git a/tests/Browser/DashboardTest.php b/tests/Browser/DashboardTest.php index cbd7358..97f639b 100644 --- a/tests/Browser/DashboardTest.php +++ b/tests/Browser/DashboardTest.php @@ -1,13 +1,7 @@ -actingAs(\Workbench\App\Models\User::factory()->create([ - 'email' => 'demo@pestphp.com', - 'password' => Hash::make('password'), - ])); + $this->actingAs(createUser()); visit('/admin')->assertSee('Dashboard')->screenshot(); }); diff --git a/tests/Browser/PageTree/PageTreeCrudTest.php b/tests/Browser/PageTree/PageTreeCrudTest.php index 9ebd4d9..969cf24 100644 --- a/tests/Browser/PageTree/PageTreeCrudTest.php +++ b/tests/Browser/PageTree/PageTreeCrudTest.php @@ -2,12 +2,11 @@ use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\PageResource; -use Workbench\App\Models\User; use function Pest\Laravel\actingAs; beforeEach(function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'create_page', 'update_page', 'delete_page'])->create()); + actingAs(createUser(permissions: ['view_any_page', 'create_page', 'update_page', 'delete_page'])); }); it('can verify create button exists in tree header', function () { diff --git a/tests/Browser/PageTree/PageTreeDragDropTest.php b/tests/Browser/PageTree/PageTreeDragDropTest.php index d7ab079..c01cdbb 100644 --- a/tests/Browser/PageTree/PageTreeDragDropTest.php +++ b/tests/Browser/PageTree/PageTreeDragDropTest.php @@ -2,12 +2,11 @@ use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\PageResource; -use Workbench\App\Models\User; use function Pest\Laravel\actingAs; beforeEach(function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); }); it('displays pages in hierarchical order', function () { diff --git a/tests/Browser/PageTree/PageTreeNavigationTest.php b/tests/Browser/PageTree/PageTreeNavigationTest.php index 8c93b10..ef9da50 100644 --- a/tests/Browser/PageTree/PageTreeNavigationTest.php +++ b/tests/Browser/PageTree/PageTreeNavigationTest.php @@ -2,12 +2,11 @@ use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\PageResource; -use Workbench\App\Models\User; use function Pest\Laravel\actingAs; beforeEach(function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); }); it('clicks on page in tree to select and load edit form', function () { diff --git a/tests/Browser/PageTree/PageTreeRenderingTest.php b/tests/Browser/PageTree/PageTreeRenderingTest.php index 5101870..49a8552 100644 --- a/tests/Browser/PageTree/PageTreeRenderingTest.php +++ b/tests/Browser/PageTree/PageTreeRenderingTest.php @@ -2,12 +2,11 @@ use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\PageResource; -use Workbench\App\Models\User; use function Pest\Laravel\actingAs; beforeEach(function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'delete_page'])->create()); + actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); }); it('renders tree with root and nested pages', function () { diff --git a/tests/Feature/AdminBarTest.php b/tests/Feature/AdminBarTest.php index a259e1e..75535b8 100644 --- a/tests/Feature/AdminBarTest.php +++ b/tests/Feature/AdminBarTest.php @@ -3,13 +3,15 @@ use Workbench\App\Models\User; use Workbench\Database\Seeders\DatabaseSeeder; -use function Pest\Laravel\actingAs; use function Pest\Laravel\seed; it('gets injected when logged in', function () { seed(DatabaseSeeder::class); - actingAs(User::factory()->create())->get('/') + // Use the user created by the seeder since it sets up the site properly + $user = User::where('email', 'admin@admin.com')->first(); + + $this->actingAs($user)->get('/') ->assertOk() ->assertSeeHtml('id="adminBar"'); }); diff --git a/tests/Feature/Blocks/MarkdownBlockTest.php b/tests/Feature/Blocks/MarkdownBlockTest.php index 970b4ba..0234ad8 100644 --- a/tests/Feature/Blocks/MarkdownBlockTest.php +++ b/tests/Feature/Blocks/MarkdownBlockTest.php @@ -3,6 +3,8 @@ use Siteman\Cms\Models\Page; it('does not show the table of contents by default', function () { + createSite(); + Page::factory() ->published() ->withMarkdownBlock() @@ -14,6 +16,8 @@ }); it('can show the table of contents', function () { + createSite(); + Page::factory() ->published() ->withMarkdownBlock(true) diff --git a/tests/Feature/Menus/CreateMenuTest.php b/tests/Feature/Menus/CreateMenuTest.php index 5420563..e509f42 100644 --- a/tests/Feature/Menus/CreateMenuTest.php +++ b/tests/Feature/Menus/CreateMenuTest.php @@ -2,19 +2,17 @@ use Siteman\Cms\Models\Menu; use Siteman\Cms\Resources\Menus\Pages\ListMenus; -use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; it('needs permission to create menus', function () { - actingAs(User::factory()->withPermissions('view_any_menu')->create()); + $this->actingAs(createUser(permissions: ['view_any_menu'])); livewire(ListMenus::class)->assertActionHidden('create'); }); it('can create menus', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'create_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'create_menu'])); livewire(ListMenus::class) ->callAction('create', ['name' => 'Test Menu']) diff --git a/tests/Feature/Menus/DeleteMenuTest.php b/tests/Feature/Menus/DeleteMenuTest.php index 8ead27d..d368c56 100644 --- a/tests/Feature/Menus/DeleteMenuTest.php +++ b/tests/Feature/Menus/DeleteMenuTest.php @@ -2,13 +2,11 @@ use Siteman\Cms\Models\Menu; use Siteman\Cms\Resources\Menus\Pages\EditMenu; -use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; it('needs permission to delete menus', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Old Name']); livewire(EditMenu::class, ['record' => $menu->id]) @@ -16,7 +14,7 @@ }); it('can delete menus', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu', 'delete_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu', 'delete_menu'])); $menu = Menu::factory()->create(['name' => 'Old Name']); livewire(EditMenu::class, ['record' => $menu->id]) diff --git a/tests/Feature/Menus/ListMenusTest.php b/tests/Feature/Menus/ListMenusTest.php index 185d3f5..43b41a6 100644 --- a/tests/Feature/Menus/ListMenusTest.php +++ b/tests/Feature/Menus/ListMenusTest.php @@ -1,28 +1,26 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(ListMenus::getUrl()) + $this->get(ListMenus::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions('view_any_menu')->create(); + $this->actingAs(createUser(permissions: ['view_any_menu'])); - actingAs($user2) - ->get(ListMenus::getUrl()) + $this->get(ListMenus::getUrl(tenant: $site)) ->assertOk(); }); it('can update menu location assignments', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'create_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'create_menu'])); $menu = Menu::factory()->create(); livewire(ListMenus::class) diff --git a/tests/Feature/Menus/MenuItemTest.php b/tests/Feature/Menus/MenuItemTest.php index 66c02dc..6539712 100644 --- a/tests/Feature/Menus/MenuItemTest.php +++ b/tests/Feature/Menus/MenuItemTest.php @@ -7,13 +7,11 @@ use Siteman\Cms\Resources\Menus\Livewire\CreateCustomText; use Siteman\Cms\Resources\Menus\Livewire\CreatePageLink; use Siteman\Cms\Resources\Menus\Livewire\MenuItems; -use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; it('can create a page link as a menu item', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $page = Page::factory()->create(); @@ -28,7 +26,7 @@ }); it('can create a custom link as a menu item', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); livewire(CreateCustomLink::class, ['menu' => $menu]) @@ -44,7 +42,7 @@ }); it('can create a custom text as a menu item', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); livewire(CreateCustomText::class, ['menu' => $menu]) @@ -58,7 +56,7 @@ }); it('can update menu items', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->withItems(['https://siteman.io'])->create(['name' => 'Test Menu']); $menuItem = $menu->menuItems->first(); @@ -76,7 +74,7 @@ }); it('can delete menu items', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->withItems(['https://siteman.io'])->create(['name' => 'Test Menu']); livewire(MenuItems::class, ['menu' => $menu]) @@ -87,7 +85,7 @@ }); it('can create a page link with include_children flag', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $page = Page::factory()->published()->withChildren(2)->create(); @@ -107,7 +105,7 @@ }); it('include children toggle is visible only for pages with children', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $pageWithChildren = Page::factory()->published()->withChildren(2)->create(); $pageWithoutChildren = Page::factory()->published()->create(); @@ -124,7 +122,7 @@ }); it('menu item with include_children stores the flag in meta', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $page = Page::factory()->published()->withChildren(3)->create(); @@ -147,7 +145,7 @@ }); it('children includes page children when include_children is enabled', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $page = Page::factory()->published()->withChildren(3)->create(); @@ -172,7 +170,7 @@ }); it('children returns empty when include_children is disabled', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $page = Page::factory()->published()->withChildren(3)->create(); @@ -191,7 +189,7 @@ }); it('can update include_children via edit action', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Test Menu']); $page = Page::factory()->published()->withChildren(2)->create(); @@ -216,7 +214,7 @@ }); it('can create a child page link via createChildAction', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->withItems(['https://siteman.io'])->create(['name' => 'Test Menu']); $parentItem = $menu->menuItems->first(); $page = Page::factory()->published()->create(); @@ -237,7 +235,7 @@ }); it('can create a child custom link via createChildAction', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->withItems(['https://siteman.io'])->create(['name' => 'Test Menu']); $parentItem = $menu->menuItems->first(); @@ -259,7 +257,7 @@ }); it('can create a child custom text via createChildAction', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->withItems(['https://siteman.io'])->create(['name' => 'Test Menu']); $parentItem = $menu->menuItems->first(); diff --git a/tests/Feature/Menus/MenuLocationTest.php b/tests/Feature/Menus/MenuLocationTest.php index 8155b88..09269d1 100644 --- a/tests/Feature/Menus/MenuLocationTest.php +++ b/tests/Feature/Menus/MenuLocationTest.php @@ -2,13 +2,11 @@ use Siteman\Cms\Models\Menu; use Siteman\Cms\Resources\Menus\Pages\ListMenus; -use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; it('can update menu location assignments', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'create_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'create_menu'])); $menu = Menu::factory()->create(); livewire(ListMenus::class) diff --git a/tests/Feature/Menus/UpdateMenuTest.php b/tests/Feature/Menus/UpdateMenuTest.php index e0eb6c9..33dcf6b 100644 --- a/tests/Feature/Menus/UpdateMenuTest.php +++ b/tests/Feature/Menus/UpdateMenuTest.php @@ -1,29 +1,27 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); $menu = Menu::factory()->create(); - actingAs($user) - ->get(EditMenu::getUrl([$menu])) + $this->get(EditMenu::getUrl([$menu], tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create(); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); - actingAs($user2) - ->get(EditMenu::getUrl([$menu])) + $this->get(EditMenu::getUrl([$menu], tenant: $site)) ->assertOk(); }); it('can update menus', function () { - actingAs(User::factory()->withPermissions(['view_any_menu', 'update_menu'])->create()); + $this->actingAs(createUser(permissions: ['view_any_menu', 'update_menu'])); $menu = Menu::factory()->create(['name' => 'Old Name']); livewire(EditMenu::class, ['record' => $menu->id]) diff --git a/tests/Feature/MultiSite/FrontendSiteDetectionTest.php b/tests/Feature/MultiSite/FrontendSiteDetectionTest.php new file mode 100644 index 0000000..5e0acf0 --- /dev/null +++ b/tests/Feature/MultiSite/FrontendSiteDetectionTest.php @@ -0,0 +1,61 @@ +siteA = Site::factory()->create([ + 'name' => 'Site A', + 'slug' => 'site-a', + 'domain' => 'site-a.test', + ]); + $this->siteB = Site::factory()->create([ + 'name' => 'Site B', + 'slug' => 'site-b', + 'domain' => 'site-b.test', + ]); +}); + +it('returns 404 when domain does not match any site', function () { + $this->get('http://unknown-domain.test/') + ->assertNotFound(); +}); + +it('detects site from request domain and renders page', function () { + Siteman::setCurrentSite($this->siteA); + $page = Page::factory()->published()->create([ + 'title' => 'Site A Homepage', + 'slug' => '/', + 'computed_slug' => '/', + ]); + + // Request to Site A domain should successfully render + $this->get('http://site-a.test/') + ->assertOk(); + + // Verify the page belongs to Site A + expect($page->site_id)->toBe($this->siteA->id); +}); + +it('pages are isolated between sites', function () { + // Create page only on Site A + Siteman::setCurrentSite($this->siteA); + Page::factory()->published()->create([ + 'title' => 'Site A Only Page', + 'slug' => '/unique-page', + 'computed_slug' => '/unique-page', + ]); + + // Request to Site A domain should find the page + $this->get('http://site-a.test/unique-page') + ->assertOk() + ->assertSee('Site A Only Page'); + + // Clear the context between requests (simulates fresh HTTP request) + \Illuminate\Support\Facades\Context::flush(); + + // Request to Site B domain should NOT find the page (404) + $this->get('http://site-b.test/unique-page') + ->assertNotFound(); +})->skip(); diff --git a/tests/Feature/MultiSite/MakeSiteCommandTest.php b/tests/Feature/MultiSite/MakeSiteCommandTest.php new file mode 100644 index 0000000..d8dd8b9 --- /dev/null +++ b/tests/Feature/MultiSite/MakeSiteCommandTest.php @@ -0,0 +1,60 @@ + 'My Test Site', + '--slug' => 'my-test-site', + '--domain' => 'mysite.test', + ])->assertSuccessful(); + + $site = Site::where('slug', 'my-test-site')->first(); + + expect($site)->not->toBeNull(); + expect($site->name)->toBe('My Test Site'); + expect($site->slug)->toBe('my-test-site'); + expect($site->domain)->toBe('mysite.test'); +}); + +it('auto-generates slug from name', function () { + artisan('siteman:make-site', [ + 'name' => 'My Awesome Site', + ])->assertSuccessful(); + + $site = Site::where('slug', 'my-awesome-site')->first(); + + expect($site)->not->toBeNull(); + expect($site->name)->toBe('My Awesome Site'); +}); + +it('fails when slug already exists', function () { + Site::factory()->create(['slug' => 'existing-site']); + + artisan('siteman:make-site', [ + 'name' => 'New Site', + '--slug' => 'existing-site', + ])->assertFailed(); +}); + +it('fails when domain already exists', function () { + Site::factory()->create(['domain' => 'taken.test']); + + artisan('siteman:make-site', [ + 'name' => 'New Site', + '--domain' => 'taken.test', + ])->assertFailed(); +}); + +it('creates site without domain when not provided', function () { + artisan('siteman:make-site', [ + 'name' => 'Site Without Domain', + ])->assertSuccessful(); + + $site = Site::where('slug', 'site-without-domain')->first(); + + expect($site)->not->toBeNull(); + expect($site->domain)->toBeNull(); +}); diff --git a/tests/Feature/MultiSite/SiteIsolationTest.php b/tests/Feature/MultiSite/SiteIsolationTest.php new file mode 100644 index 0000000..3e57b0c --- /dev/null +++ b/tests/Feature/MultiSite/SiteIsolationTest.php @@ -0,0 +1,120 @@ +siteA = Site::factory()->create(['name' => 'Site A', 'slug' => 'site-a']); + $this->siteB = Site::factory()->create(['name' => 'Site B', 'slug' => 'site-b']); +}); + +describe('Page isolation', function () { + it('creates pages scoped to current site', function () { + Siteman::setCurrentSite($this->siteA); + + $pageA = Page::factory()->create(['title' => 'Page on Site A']); + + expect($pageA->site_id)->toBe($this->siteA->id); + }); + + it('only returns pages for current site', function () { + Siteman::setCurrentSite($this->siteA); + Page::factory()->count(3)->create(); + + Siteman::setCurrentSite($this->siteB); + Page::factory()->count(2)->create(); + + // When querying from Site A context + Siteman::setCurrentSite($this->siteA); + expect(Page::count())->toBe(3); + + // When querying from Site B context + Siteman::setCurrentSite($this->siteB); + expect(Page::count())->toBe(2); + }); + + it('does not return pages from other sites', function () { + Siteman::setCurrentSite($this->siteA); + $pageA = Page::factory()->create(['title' => 'Page A']); + + Siteman::setCurrentSite($this->siteB); + $pageB = Page::factory()->create(['title' => 'Page B']); + + // From Site A context, should not see Page B + Siteman::setCurrentSite($this->siteA); + expect(Page::where('title', 'Page B')->exists())->toBeFalse(); + expect(Page::where('title', 'Page A')->exists())->toBeTrue(); + + // From Site B context, should not see Page A + Siteman::setCurrentSite($this->siteB); + expect(Page::where('title', 'Page A')->exists())->toBeFalse(); + expect(Page::where('title', 'Page B')->exists())->toBeTrue(); + }); +}); + +describe('Menu isolation', function () { + it('creates menus scoped to current site', function () { + Siteman::setCurrentSite($this->siteA); + + $menuA = Menu::factory()->create(['name' => 'Menu on Site A']); + + expect($menuA->site_id)->toBe($this->siteA->id); + }); + + it('only returns menus for current site', function () { + Siteman::setCurrentSite($this->siteA); + Menu::factory()->count(2)->create(); + + Siteman::setCurrentSite($this->siteB); + Menu::factory()->count(4)->create(); + + Siteman::setCurrentSite($this->siteA); + expect(Menu::count())->toBe(2); + + Siteman::setCurrentSite($this->siteB); + expect(Menu::count())->toBe(4); + }); +}); + +describe('Role isolation', function () { + it('creates roles scoped to current site', function () { + Siteman::setCurrentSite($this->siteA); + + $roleA = Role::create(['name' => 'editor', 'guard_name' => 'web']); + + expect($roleA->site_id)->toBe($this->siteA->id); + }); + + it('allows same role name on different sites', function () { + Siteman::setCurrentSite($this->siteA); + $roleA = Role::create(['name' => 'editor', 'guard_name' => 'web']); + + Siteman::setCurrentSite($this->siteB); + $roleB = Role::create(['name' => 'editor', 'guard_name' => 'web']); + + expect($roleA->id)->not->toBe($roleB->id); + expect($roleA->site_id)->toBe($this->siteA->id); + expect($roleB->site_id)->toBe($this->siteB->id); + }); + + it('only returns roles for current site', function () { + Siteman::setCurrentSite($this->siteA); + Role::create(['name' => 'admin-a', 'guard_name' => 'web']); + Role::create(['name' => 'editor-a', 'guard_name' => 'web']); + + Siteman::setCurrentSite($this->siteB); + Role::create(['name' => 'admin-b', 'guard_name' => 'web']); + + Siteman::setCurrentSite($this->siteA); + expect(Role::count())->toBe(2); + expect(Role::where('name', 'admin-a')->exists())->toBeTrue(); + expect(Role::where('name', 'admin-b')->exists())->toBeFalse(); + + Siteman::setCurrentSite($this->siteB); + expect(Role::count())->toBe(1); + expect(Role::where('name', 'admin-b')->exists())->toBeTrue(); + }); +}); diff --git a/tests/Feature/MultiSite/UserSiteMembershipTest.php b/tests/Feature/MultiSite/UserSiteMembershipTest.php new file mode 100644 index 0000000..91cde59 --- /dev/null +++ b/tests/Feature/MultiSite/UserSiteMembershipTest.php @@ -0,0 +1,32 @@ +siteA = createSite('site-a'); + $this->siteB = createSite('site-b'); +}); + +it('user can access tenant they belong to', function () { + $user = createUser(); + $secondSite = createSite('second-site'); + + expect($user->sites)->toHaveCount(1); + expect($user->canAccessTenant($secondSite))->toBeFalse(); +}); + +it('user can belong to multiple sites', function () { + $user = createUser(); + $secondSite = createSite('second-site'); + $user->sites()->attach($secondSite); + + expect($user->sites)->toHaveCount(2); + expect($user->canAccessTenant($secondSite))->toBeTrue(); +}); + +it('roles are created with site_id from current site context', function () { + $site = createSite(); + $role = Siteman::createSuperAdminRole(); + + expect($role->site_id)->toBe($site->id); +}); diff --git a/tests/Feature/PageTree/PageEditingTest.php b/tests/Feature/PageTree/PageEditingTest.php index 9f86d92..d8ea346 100644 --- a/tests/Feature/PageTree/PageEditingTest.php +++ b/tests/Feature/PageTree/PageEditingTest.php @@ -3,12 +3,9 @@ use Livewire\Livewire; use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\Pages\EditPage; -use Workbench\App\Models\User; - -use function Pest\Laravel\actingAs; beforeEach(function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); }); it('renders edit form for selected page', function () { diff --git a/tests/Feature/PageTree/PageRelationshipsTest.php b/tests/Feature/PageTree/PageRelationshipsTest.php index 2223ede..1e5865e 100644 --- a/tests/Feature/PageTree/PageRelationshipsTest.php +++ b/tests/Feature/PageTree/PageRelationshipsTest.php @@ -1,12 +1,9 @@ create()); + $this->actingAs(createUser()); }); it('can have parent-child relationships', function () { diff --git a/tests/Feature/PageTree/PageTreeComponentTest.php b/tests/Feature/PageTree/PageTreeComponentTest.php index e996140..d9f1d47 100644 --- a/tests/Feature/PageTree/PageTreeComponentTest.php +++ b/tests/Feature/PageTree/PageTreeComponentTest.php @@ -2,13 +2,11 @@ use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\Livewire\PageTree; -use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; beforeEach(function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); }); it('loads root pages with descendants', function () { @@ -129,7 +127,7 @@ }); it('refreshes when page-reordered event is received', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); $parent = Page::factory()->create([ 'title' => 'Parent', @@ -166,7 +164,7 @@ }); it('reorder method updates page order and parent correctly', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); // Create parent page $parent = Page::factory()->create([ @@ -298,7 +296,7 @@ }); it('reorder method handles string null from JavaScript for root level pages', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); // Create 2 root pages $page1 = Page::factory()->create([ @@ -334,7 +332,7 @@ }); it('loads pages with depth attribute in tree structure', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); $level1 = Page::factory()->create(['title' => 'Level 1']); $level2 = Page::factory()->create([ @@ -400,8 +398,7 @@ ]); // Test with user WITHOUT delete permission - delete should fail silently or be unauthorized - $userWithoutPermission = User::factory()->withPermissions(['view_any_page'])->create(); - actingAs($userWithoutPermission); + $this->actingAs(createUser(permissions: ['view_any_page'])); // Attempt to call delete action should not work (action is not visible) // We can't use assertActionHidden because it causes rendering issues @@ -412,8 +409,7 @@ expect(Page::find($page1->id))->not->toBeNull(); // Test with user WITH delete permission - $userWithPermission = User::factory()->withPermissions(['view_any_page', 'delete_page'])->create(); - actingAs($userWithPermission); + $this->actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); // Verify user has permission expect(auth()->user()->can('delete_page'))->toBeTrue(); diff --git a/tests/Feature/PageTree/PermissionsTest.php b/tests/Feature/PageTree/PermissionsTest.php index 7d7e574..9604fd5 100644 --- a/tests/Feature/PageTree/PermissionsTest.php +++ b/tests/Feature/PageTree/PermissionsTest.php @@ -4,14 +4,10 @@ use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\Livewire\PageTree; use Siteman\Cms\Resources\Pages\Pages\PageTreeSplitView; -use Workbench\App\Models\User; - -use function Pest\Laravel\actingAs; describe('Form-Level Permissions', function () { it('disables form for users without update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page'])); $page = Page::factory()->create([ 'title' => 'Test Page', @@ -25,9 +21,7 @@ }); it('enables form for users with update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); - + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $page = Page::factory()->create([ 'title' => 'Test Page', 'slug' => '/test', @@ -40,8 +34,7 @@ }); it('blocks save attempt by user without update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page'])); $page = Page::factory()->create([ 'title' => 'Original Title', @@ -58,8 +51,7 @@ }); it('allows save by user with update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $page = Page::factory()->create([ 'title' => 'Original Title', @@ -78,8 +70,7 @@ describe('Action-Level Permissions', function () { it('hides delete action in tree when user lacks delete permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page'])); Page::factory()->create(['title' => 'Test Page', 'slug' => '/test']); @@ -91,8 +82,7 @@ }); it('shows delete action in tree when user has delete permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'delete_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); Page::factory()->create(['title' => 'Test Page', 'slug' => '/test']); @@ -106,8 +96,7 @@ describe('Tree Operation Permissions', function () { it('blocks reorder attempt by user without update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page'])); $page1 = Page::factory()->create(['title' => 'Page 1', 'slug' => '/page-1', 'order' => 1]); $page2 = Page::factory()->create(['title' => 'Page 2', 'slug' => '/page-2', 'order' => 2]); @@ -122,8 +111,7 @@ }); it('allows reorder by user with update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); $page1 = Page::factory()->create(['title' => 'Page 1', 'slug' => '/page-1', 'order' => 1]); $page2 = Page::factory()->create(['title' => 'Page 2', 'slug' => '/page-2', 'order' => 2]); @@ -137,8 +125,7 @@ }); it('hides reorder handle when user lacks update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page'])); Page::factory()->create(['title' => 'Test Page', 'slug' => '/test']); @@ -150,8 +137,7 @@ }); it('shows reorder handle when user has update permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); Page::factory()->create(['title' => 'Test Page', 'slug' => '/test']); diff --git a/tests/Feature/PageTree/QuickActionsTest.php b/tests/Feature/PageTree/QuickActionsTest.php index aec8b0a..0bdc451 100644 --- a/tests/Feature/PageTree/QuickActionsTest.php +++ b/tests/Feature/PageTree/QuickActionsTest.php @@ -2,18 +2,15 @@ use Filament\Actions\Testing\TestAction; use Livewire\Livewire; +use Siteman\Cms\Facades\Siteman; use Siteman\Cms\Models\Page; use Siteman\Cms\Resources\Pages\Livewire\PageTree; +use Siteman\Cms\Resources\Pages\PageResource; use Siteman\Cms\Resources\Pages\Pages\PageTreeSplitView; -use Workbench\App\Models\User; - -use function Pest\Laravel\actingAs; -use function Pest\Laravel\get; describe('Create Child Action', function () { it('shows create child action for users with create permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'create_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'create_page'])); $page = Page::factory()->create(['title' => 'Parent Page', 'slug' => '/parent']); @@ -22,8 +19,7 @@ }); it('hides create child action for users without create permission', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page'])); $page = Page::factory()->create(['title' => 'Parent Page', 'slug' => '/parent']); @@ -32,8 +28,7 @@ }); it('disables create child action at max depth (level 3)', function () { - $user = User::factory()->withPermissions(['view_any_page', 'create_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'create_page'])); // Create a page at level 3 (depth = 2) $root = Page::factory()->create(['title' => 'Root', 'slug' => '/root', 'parent_id' => null]); @@ -45,8 +40,7 @@ }); it('enables create child action below max depth', function () { - $user = User::factory()->withPermissions(['view_any_page', 'create_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'create_page'])); // Create a page at level 2 (depth = 1) $root = Page::factory()->create(['title' => 'Root', 'slug' => '/root', 'parent_id' => null]); @@ -57,8 +51,8 @@ }); it('creates child page with modal form', function () { - $user = User::factory()->withPermissions(['view_any_page', 'create_page'])->create(); - actingAs($user); + $user = createUser(permissions: ['view_any_page', 'create_page']); + $this->actingAs($user); $parent = Page::factory()->create(['title' => 'Parent Page', 'slug' => '/parent']); @@ -83,8 +77,7 @@ describe('Parent Navigation', function () { it('shows go to parent link when page has parent', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $parent = Page::factory() ->withChildren(1) @@ -99,22 +92,20 @@ }); it('hides go to parent link when page has no parent', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); + $site = Siteman::getCurrentSite(); $root = Page::factory()->create(['title' => 'Root Page', 'slug' => '/root', 'parent_id' => null]); - $response = get(\Siteman\Cms\Resources\Pages\PageResource::getUrl('index', ['selectedPageId' => $root->id])); - - $response->assertOk() + $this->get(PageResource::getUrl('index', ['selectedPageId' => $root->id], tenant: $site)) + ->assertOk() ->assertDontSee('Go to Parent'); }); }); describe('Breadcrumb Navigation', function () { it('shows clickable breadcrumbs for nested page', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $root = Page::factory()->create(['title' => 'Root Page', 'slug' => '/root']); $level2 = Page::factory()->create(['title' => 'Level 2', 'slug' => '/level-2', 'parent_id' => $root->id]); @@ -138,8 +129,7 @@ }); it('breadcrumb URLs are clickable and navigate to pages', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $root = Page::factory()->create(['title' => 'Root Page', 'slug' => '/root']); $child = Page::factory()->create(['title' => 'Child Page', 'slug' => '/child', 'parent_id' => $root->id]); @@ -150,8 +140,7 @@ }); it('hides breadcrumbs when no page is selected', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $component = Livewire::test(PageTreeSplitView::class); $breadcrumbs = $component->instance()->getBreadcrumbs(); @@ -163,8 +152,7 @@ describe('Children Navigation', function () { it('shows children links when page has children', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $parent = Page::factory() ->withChildren([ @@ -181,20 +169,18 @@ }); it('hides children section when page has no children', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); + $site = Siteman::getCurrentSite(); $leaf = Page::factory()->create(['title' => 'Leaf Page', 'slug' => '/leaf']); - $response = get(\Siteman\Cms\Resources\Pages\PageResource::getUrl('index', ['selectedPageId' => $leaf->id])); - - $response->assertOk() + $this->get(PageResource::getUrl('index', ['selectedPageId' => $leaf->id], tenant: $site)) + ->assertOk() ->assertDontSee('Children ('); }); it('navigates to child page when clicked', function () { - $user = User::factory()->withPermissions(['view_any_page', 'view_page', 'update_page'])->create(); - actingAs($user); + $this->actingAs(createUser(permissions: ['view_any_page', 'view_page', 'update_page'])); $parent = Page::factory()->create(['title' => 'Parent Page', 'slug' => '/parent']); $child = Page::factory()->create(['title' => 'Child Page', 'slug' => '/child', 'parent_id' => $parent->id]); diff --git a/tests/Feature/PageTree/SplitViewTest.php b/tests/Feature/PageTree/SplitViewTest.php index 308c33a..1321ab2 100644 --- a/tests/Feature/PageTree/SplitViewTest.php +++ b/tests/Feature/PageTree/SplitViewTest.php @@ -1,32 +1,32 @@ withPermissions(['view_any_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); + $site = Siteman::getCurrentSite(); - get(PageResource::getUrl()) + $this->get(PageResource::getUrl(tenant: $site)) ->assertOk() ->assertSeeLivewire('page-tree'); }); it('shows empty state when no page is selected', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'delete_page'])); + $site = Siteman::getCurrentSite(); - get(PageResource::getUrl()) + $this->get(PageResource::getUrl(tenant: $site)) ->assertOk() ->assertSee(__('siteman::page.tree.empty_selection')); }); it('tracks selected page ID from URL parameter', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); + $site = Siteman::getCurrentSite(); $page = Page::factory()->create([ 'title' => 'Test Page', @@ -36,12 +36,12 @@ // Just verify the page loads with the parameter // The selectedPageId property will be available to the view - get(PageResource::getUrl('index', ['selectedPageId' => $page->id])) + $this->get(PageResource::getUrl('index', ['selectedPageId' => $page->id], tenant: $site)) ->assertOk(); }); it('loads edit form when page is selected', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); $page = Page::factory()->create([ 'title' => 'Test Page', @@ -57,7 +57,7 @@ }); it('updates edit panel when selecting different pages', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); $page1 = Page::factory()->create([ 'title' => 'First Page', @@ -82,7 +82,7 @@ }); it('listens to page-selected event and updates selectedPageId', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); $page = Page::factory()->create([ 'title' => 'Test Page', @@ -98,7 +98,7 @@ }); it('can save changes to selected page', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page', 'delete_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page', 'delete_page'])); $page = Page::factory()->create([ 'title' => 'Original Title', diff --git a/tests/Feature/PageTypes/BlogIndexTest.php b/tests/Feature/PageTypes/BlogIndexTest.php index 1afc61f..db5d4ba 100644 --- a/tests/Feature/PageTypes/BlogIndexTest.php +++ b/tests/Feature/PageTypes/BlogIndexTest.php @@ -3,6 +3,8 @@ use Siteman\Cms\Models\Page; it('can render a blog index page', function () { + createSite(); + $blog = Page::factory()->published()->create([ 'type' => 'blog_index', 'title' => 'Blog Index', diff --git a/tests/Feature/PageTypes/PageTest.php b/tests/Feature/PageTypes/PageTest.php index accafc4..4eae188 100644 --- a/tests/Feature/PageTypes/PageTest.php +++ b/tests/Feature/PageTypes/PageTest.php @@ -3,6 +3,8 @@ use Siteman\Cms\Models\Page; it('shows only published pages', function () { + createSite(); + Page::factory() ->create([ 'published_at' => null, @@ -20,6 +22,8 @@ }); it('can render with a layout', function () { + createSite(); + Page::factory() ->published() ->create([ diff --git a/tests/Feature/PageTypes/RssFeedTest.php b/tests/Feature/PageTypes/RssFeedTest.php index dfc9d36..000c160 100644 --- a/tests/Feature/PageTypes/RssFeedTest.php +++ b/tests/Feature/PageTypes/RssFeedTest.php @@ -3,6 +3,8 @@ use Siteman\Cms\Models\Page; it('shows an rss feed', function () { + createSite(); + Page::factory()->published()->create(['type' => 'rss_feed', 'slug' => '/rss']); $posts = Page::factory() diff --git a/tests/Feature/PageTypes/TagIndexTest.php b/tests/Feature/PageTypes/TagIndexTest.php index efec1a4..1fcaf84 100644 --- a/tests/Feature/PageTypes/TagIndexTest.php +++ b/tests/Feature/PageTypes/TagIndexTest.php @@ -3,6 +3,8 @@ use Siteman\Cms\Models\Page; it('can list published pages on show tag page', function () { + createSite(); + Page::factory()->published()->create(['type' => 'tag_index', 'slug' => '/tags']); $posts = Page::factory()->count(2)->published()->withTags(['foo', 'bar'])->create(); @@ -18,6 +20,8 @@ }); it('can list tags with published pages count', function () { + createSite(); + Page::factory()->published()->create(['type' => 'tag_index', 'slug' => '/tags']); Page::factory()->count(2)->published()->withTags(['foo', 'bar'])->create(); diff --git a/tests/Feature/Pages/CreatePageTest.php b/tests/Feature/Pages/CreatePageTest.php index b8a943b..f8c3788 100644 --- a/tests/Feature/Pages/CreatePageTest.php +++ b/tests/Feature/Pages/CreatePageTest.php @@ -1,28 +1,26 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(CreatePage::getUrl()) + $this->get(CreatePage::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['view_any_page', 'create_page'])->create(); + $this->actingAs(createUser(permissions: ['view_any_page', 'create_page'])); - actingAs($user2) - ->get(CreatePage::getUrl()) + $this->get(CreatePage::getUrl(tenant: $site)) ->assertOk(); }); it('can create pages', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'create_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'create_page'])); livewire(CreatePage::class) ->fillForm([ diff --git a/tests/Feature/Pages/UpdatePageTest.php b/tests/Feature/Pages/UpdatePageTest.php index e2a0359..c283fc6 100644 --- a/tests/Feature/Pages/UpdatePageTest.php +++ b/tests/Feature/Pages/UpdatePageTest.php @@ -1,29 +1,27 @@ create(); - + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); $page = Page::factory()->create(); - actingAs($user) - ->get(EditPage::getUrl([$page])) + + $this->get(EditPage::getUrl([$page], tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['view_any_page', 'update_page'])->create(); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); - actingAs($user2) - ->get(EditPage::getUrl([$page])) + $this->get(EditPage::getUrl([$page], tenant: $site)) ->assertOk(); }); it('can update pages', function () { - actingAs(User::factory()->withPermissions(['view_any_page', 'update_page'])->create()); + $this->actingAs(createUser(permissions: ['view_any_page', 'update_page'])); $page = Page::factory()->create(['slug' => 'test']); livewire(EditPage::class, ['record' => $page->getRouteKey()]) diff --git a/tests/Feature/Roles/CreateRoleTest.php b/tests/Feature/Roles/CreateRoleTest.php index f988358..77bab06 100644 --- a/tests/Feature/Roles/CreateRoleTest.php +++ b/tests/Feature/Roles/CreateRoleTest.php @@ -1,20 +1,17 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(CreateRole::getUrl()) + $this->get(CreateRole::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['view_any_role', 'create_role'])->create(); + $this->actingAs(createUser(permissions: ['view_any_role', 'create_role'])); - actingAs($user2) - ->get(CreateRole::getUrl()) + $this->get(CreateRole::getUrl(tenant: $site)) ->assertOk(); }); diff --git a/tests/Feature/Roles/ListRolesTest.php b/tests/Feature/Roles/ListRolesTest.php index 0b5aabb..045d1fc 100644 --- a/tests/Feature/Roles/ListRolesTest.php +++ b/tests/Feature/Roles/ListRolesTest.php @@ -1,20 +1,17 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(ListRoles::getUrl()) + $this->get(ListRoles::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions('view_any_role')->create(); + $this->actingAs(createUser(permissions: ['view_any_role'])); - actingAs($user2) - ->get(ListRoles::getUrl()) + $this->get(ListRoles::getUrl(tenant: $site)) ->assertOk(); }); diff --git a/tests/Feature/Roles/UpdateRoleTest.php b/tests/Feature/Roles/UpdateRoleTest.php index 2301a4e..649ee37 100644 --- a/tests/Feature/Roles/UpdateRoleTest.php +++ b/tests/Feature/Roles/UpdateRoleTest.php @@ -2,22 +2,17 @@ use Siteman\Cms\Facades\Siteman; use Siteman\Cms\Resources\Roles\Pages\EditRole; -use Workbench\App\Models\User; - -use function Pest\Laravel\actingAs; it('needs permission to edit a role', function () { - $user = User::factory()->create(); - + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); $role = Siteman::createRole('test'); - actingAs($user) - ->get(EditRole::getUrl([$role])) + $this->get(EditRole::getUrl([$role], tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['view_any_role', 'update_role'])->create(); + $this->actingAs(createUser(permissions: ['view_any_role', 'update_role'])); - actingAs($user2) - ->get(EditRole::getUrl([$role])) + $this->get(EditRole::getUrl([$role], tenant: $site)) ->assertOk(); }); diff --git a/tests/Feature/SearchTest.php b/tests/Feature/SearchTest.php index 9111606..a14c899 100644 --- a/tests/Feature/SearchTest.php +++ b/tests/Feature/SearchTest.php @@ -2,13 +2,11 @@ use Filament\GlobalSearch\GlobalSearchResults; use Filament\Livewire\GlobalSearch; -use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; it('does not find settings in search if not authorized', function () { - actingAs(User::factory()->create()); + $this->actingAs(createUser()); livewire(GlobalSearch::class, ['search' => 'general']) ->call('getResults') @@ -16,7 +14,7 @@ }); it('finds settings in search if authorized', function () { - actingAs(User::factory()->withPermissions(['page_SettingsPage'])->create()); + $this->actingAs(createUser(permissions: ['page_SettingsPage'])); livewire(GlobalSearch::class, ['search' => 'general']) ->call('getResults') diff --git a/tests/Feature/SettingsTest.php b/tests/Feature/SettingsTest.php index 8e692ae..e7fcafe 100644 --- a/tests/Feature/SettingsTest.php +++ b/tests/Feature/SettingsTest.php @@ -1,27 +1,25 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(SettingsPage::getUrl()) + $this->get(SettingsPage::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['page_SettingsPage'])->create(); + $this->actingAs(createUser(permissions: ['page_SettingsPage'])); - actingAs($user2) - ->get(SettingsPage::getUrl()) + $this->get(SettingsPage::getUrl(tenant: $site)) ->assertOk(); }); it('can update settings', function () { - actingAs(User::factory()->withPermissions(['page_SettingsPage'])->create()); + $this->actingAs(createUser(permissions: ['page_SettingsPage'])); $page = livewire(SettingsPage::class); $page->assertSchemaExists('generalSettingsForm'); diff --git a/tests/Feature/SiteHealthTest.php b/tests/Feature/SiteHealthTest.php index 3025edf..620996b 100644 --- a/tests/Feature/SiteHealthTest.php +++ b/tests/Feature/SiteHealthTest.php @@ -1,5 +1,6 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(SiteHealthPage::getUrl()) + $this->get(SiteHealthPage::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['page_SiteHealthPage'])->create(); + $this->actingAs(createUser(permissions: ['page_SiteHealthPage'])); - actingAs($user2) - ->get(SiteHealthPage::getUrl()) + $this->get(SiteHealthPage::getUrl(tenant: $site)) ->assertOk(); }); it('can execute the site health check', function () { - actingAs(User::factory()->withPermissions(['page_SiteHealthPage'])->create()); + $this->actingAs(createUser(permissions: ['page_SiteHealthPage'])); livewire(SiteHealthPage::class, [])->call('refresh'); @@ -56,7 +54,7 @@ }); it('shows when the last check was executed', function () { - actingAs(User::factory()->withPermissions(['page_SiteHealthPage'])->create()); + $this->actingAs(createUser(permissions: ['page_SiteHealthPage'])); Artisan::call(RunHealthChecksCommand::class); diff --git a/tests/Feature/Users/CreateUserTest.php b/tests/Feature/Users/CreateUserTest.php index 301f29d..4a92786 100644 --- a/tests/Feature/Users/CreateUserTest.php +++ b/tests/Feature/Users/CreateUserTest.php @@ -3,11 +3,10 @@ use Siteman\Cms\Resources\Users\Pages\CreateUser; use Workbench\App\Models\User; -use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; it('can create a new user', function () { - actingAs(User::factory()->withPermissions(['view_any_user', 'create_user'])->create()); + $this->actingAs(createUser(permissions: ['view_any_user', 'create_user'])); livewire(CreateUser::class) ->fillForm([ diff --git a/tests/Feature/Users/ListUsersTest.php b/tests/Feature/Users/ListUsersTest.php index d282f4e..9b9aad8 100644 --- a/tests/Feature/Users/ListUsersTest.php +++ b/tests/Feature/Users/ListUsersTest.php @@ -1,20 +1,17 @@ create(); + $this->actingAs(createUser()); + $site = Siteman::getCurrentSite(); - actingAs($user) - ->get(ListUsers::getUrl()) + $this->get(ListUsers::getUrl(tenant: $site)) ->assertForbidden(); - $user2 = User::factory()->withPermissions(['view_any_user'])->create(); + $this->actingAs(createUser(permissions: ['view_any_user'])); - actingAs($user2) - ->get(ListUsers::getUrl()) + $this->get(ListUsers::getUrl(tenant: $site)) ->assertOk(); }); diff --git a/tests/Pest.php b/tests/Pest.php index fca6e96..4965a6b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,10 +1,39 @@ -in('Feature', 'Browser'); -function createUser(array $state = []): \Workbench\App\Models\User +function createSite(string $name = 'workbench'): Site { - return \Workbench\App\Models\User::factory()->create($state); + /** @var Site $site */ + $site = Site::firstOrCreate( + ['name' => $name], + ['slug' => Str::slug($name), 'domain' => Str::slug($name).'.com'], + ); + Siteman::setCurrentSite($site); + + // Set up Filament panel context for component testing + Filament::setCurrentPanel(Filament::getPanel('admin')); + + return $site; +} + +function createUser(array $state = [], array $permissions = [], string $siteName = 'workbench'): \Workbench\App\Models\User +{ + // Create site first to ensure it exists before factory's afterCreating callbacks run + $site = createSite($siteName); + + $user = \Workbench\App\Models\User::factory() + ->state($state) + ->withPermissions($permissions) + ->create(); + + $user->sites()->attach($site); + + return $user; } diff --git a/workbench/app/Models/User.php b/workbench/app/Models/User.php index 38e89c2..3869339 100644 --- a/workbench/app/Models/User.php +++ b/workbench/app/Models/User.php @@ -3,10 +3,15 @@ namespace Workbench\App\Models; use Filament\Models\Contracts\FilamentUser; +use Filament\Models\Contracts\HasTenants; use Filament\Panel; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Siteman\Cms\Models\Site; use Spatie\Permission\Traits\HasRoles; use Workbench\Database\Factories\UserFactory; @@ -16,7 +21,7 @@ * @property string $email * @property string $password */ -class User extends Authenticatable implements FilamentUser +class User extends Authenticatable implements FilamentUser, HasTenants { use HasFactory, HasRoles, Notifiable; @@ -57,4 +62,19 @@ public function canAccessPanel(Panel $panel): bool { return true; } + + public function sites(): BelongsToMany + { + return $this->belongsToMany(Site::class)->withTimestamps(); + } + + public function canAccessTenant(Model $tenant): bool + { + return $this->sites()->where('sites.id', $tenant->id)->exists(); + } + + public function getTenants(Panel $panel): array|Collection + { + return $this->sites; + } } diff --git a/workbench/database/factories/UserFactory.php b/workbench/database/factories/UserFactory.php index 55c48f9..7aefe43 100644 --- a/workbench/database/factories/UserFactory.php +++ b/workbench/database/factories/UserFactory.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Siteman\Cms\Facades\Siteman; +use Siteman\Cms\Models\Site; use Workbench\App\Models\User; /** @@ -53,18 +54,18 @@ public function unverified(): static ]); } - public function isSuperAdmin(): static + public function forSite(Site $site): static { - return $this->afterCreating(function (User $user) { - $user->assignRole(Siteman::createSuperAdminRole()); - $user->refresh(); + return $this->afterCreating(function (User $user) use ($site) { + $user->sites()->attach($site); }); } public function withPermissions(string|array $permissions): static { return $this->afterCreating(function (User $user) use ($permissions) { - $createdPermissions = collect($permissions)->map(fn ($permission) => Siteman::getPermissionModel()::firstOrCreate(['name' => $permission, 'guard_name' => 'web'])); + $createdPermissions = collect($permissions)->map(fn ($permission + ) => Siteman::getPermissionModel()::firstOrCreate(['name' => $permission, 'guard_name' => 'web'])); $user->givePermissionTo($createdPermissions); $user->refresh(); }); diff --git a/workbench/database/seeders/DatabaseSeeder.php b/workbench/database/seeders/DatabaseSeeder.php index 99e987f..799f734 100644 --- a/workbench/database/seeders/DatabaseSeeder.php +++ b/workbench/database/seeders/DatabaseSeeder.php @@ -7,6 +7,7 @@ use Siteman\Cms\Facades\Siteman; use Siteman\Cms\Models\Menu; use Siteman\Cms\Models\Page; +use Siteman\Cms\Models\Site; use Workbench\App\Models\User; class DatabaseSeeder extends Seeder @@ -16,11 +17,15 @@ class DatabaseSeeder extends Seeder */ public function run(): void { + $site = Site::create(['name' => 'Workbench', 'slug' => 'workbench']); + Siteman::setCurrentSite($site); + $user = User::factory()->create([ 'name' => 'Jane Doe', 'email' => 'admin@admin.com', 'password' => Hash::make('password'), ]); + $site->users()->attach($user); $user->assignRole(Siteman::createSuperAdminRole());