service = app(NamespaceService::class); // Create users $this->user = User::factory()->create(['name' => 'Test User']); $this->otherUser = User::factory()->create(['name' => 'Other User']); // Create workspaces $this->workspace = Workspace::factory()->create(['name' => 'Test Workspace']); $this->otherWorkspace = Workspace::factory()->create(['name' => 'Other Workspace']); // Attach users to workspaces $this->user->hostWorkspaces()->attach($this->workspace, [ 'role' => 'owner', 'is_default' => true, ]); $this->otherUser->hostWorkspaces()->attach($this->otherWorkspace, [ 'role' => 'owner', 'is_default' => true, ]); // Create user-owned namespace $this->userNamespace = Namespace_::create([ 'name' => 'Personal', 'slug' => 'personal', 'owner_type' => User::class, 'owner_id' => $this->user->id, 'is_default' => true, 'is_active' => true, 'sort_order' => 0, ]); // Create workspace-owned namespace $this->workspaceNamespace = Namespace_::create([ 'name' => 'Agency Client', 'slug' => 'agency-client', 'owner_type' => Workspace::class, 'owner_id' => $this->workspace->id, 'workspace_id' => $this->workspace->id, 'is_active' => true, 'sort_order' => 0, ]); }); // ───────────────────────────────────────────────────────────────────────────── // current() resolution // ───────────────────────────────────────────────────────────────────────────── describe('current namespace resolution', function () { it('returns namespace from request attributes when set', function () { request()->attributes->set('current_namespace', $this->userNamespace); $this->actingAs($this->user); $current = $this->service->current(); expect($current)->not->toBeNull(); expect($current->id)->toBe($this->userNamespace->id); }); it('returns namespace from session UUID when user has access', function () { $this->actingAs($this->user); session(['current_namespace_uuid' => $this->userNamespace->uuid]); $current = $this->service->current(); expect($current)->not->toBeNull(); expect($current->id)->toBe($this->userNamespace->id); }); it('falls back to default namespace when session UUID is missing', function () { $this->actingAs($this->user); $current = $this->service->current(); expect($current)->not->toBeNull(); expect($current->id)->toBe($this->userNamespace->id); }); it('rejects session namespace when user lacks access', function () { $this->actingAs($this->otherUser); // Other user tries to access this user's namespace via session session(['current_namespace_uuid' => $this->userNamespace->uuid]); $current = $this->service->current(); // Should not return the inaccessible namespace if ($current !== null) { expect($current->id)->not->toBe($this->userNamespace->id); } else { expect($current)->toBeNull(); } }); it('returns null when no user is authenticated', function () { $current = $this->service->current(); expect($current)->toBeNull(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Session management // ───────────────────────────────────────────────────────────────────────────── describe('session management', function () { it('sets current namespace UUID in session from model', function () { $this->service->setCurrent($this->userNamespace); expect(session('current_namespace_uuid'))->toBe($this->userNamespace->uuid); }); it('sets current namespace UUID in session from string', function () { $uuid = $this->userNamespace->uuid; $this->service->setCurrent($uuid); expect(session('current_namespace_uuid'))->toBe($uuid); }); it('returns current UUID from session', function () { session(['current_namespace_uuid' => $this->userNamespace->uuid]); expect($this->service->currentUuid())->toBe($this->userNamespace->uuid); }); it('returns null UUID when session is empty', function () { expect($this->service->currentUuid())->toBeNull(); }); it('clears current namespace from session', function () { session(['current_namespace_uuid' => $this->userNamespace->uuid]); $this->service->clearCurrent(); expect(session('current_namespace_uuid'))->toBeNull(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Lookup methods (findByUuid, findBySlug) // ───────────────────────────────────────────────────────────────────────────── describe('namespace lookup', function () { it('finds namespace by UUID', function () { $found = $this->service->findByUuid($this->userNamespace->uuid); expect($found)->not->toBeNull(); expect($found->id)->toBe($this->userNamespace->id); }); it('returns null for unknown UUID', function () { $found = $this->service->findByUuid('non-existent-uuid'); expect($found)->toBeNull(); }); it('caches namespace by UUID', function () { // First call populates cache $this->service->findByUuid($this->userNamespace->uuid); // Verify cache key exists expect(Cache::has("namespace:uuid:{$this->userNamespace->uuid}"))->toBeTrue(); }); it('finds namespace by slug within user context', function () { $found = $this->service->findBySlug('personal', $this->user); expect($found)->not->toBeNull(); expect($found->id)->toBe($this->userNamespace->id); }); it('finds namespace by slug within workspace context', function () { $found = $this->service->findBySlug('agency-client', $this->workspace); expect($found)->not->toBeNull(); expect($found->id)->toBe($this->workspaceNamespace->id); }); it('returns null for slug in wrong owner context', function () { // Looking for a user namespace slug against the workspace owner $found = $this->service->findBySlug('personal', $this->workspace); expect($found)->toBeNull(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Default namespace resolution // ───────────────────────────────────────────────────────────────────────────── describe('default namespace for user', function () { it('returns the default namespace marked is_default', function () { $default = $this->service->defaultForUser($this->user); expect($default)->not->toBeNull(); expect($default->id)->toBe($this->userNamespace->id); expect($default->is_default)->toBeTrue(); }); it('falls back to first active user-owned namespace when no explicit default', function () { // Remove the is_default flag $this->userNamespace->update(['is_default' => false]); $default = $this->service->defaultForUser($this->user); expect($default)->not->toBeNull(); expect($default->id)->toBe($this->userNamespace->id); }); it('falls back to workspace namespace when user has no owned namespaces', function () { // Delete the user-owned namespace $this->userNamespace->forceDelete(); $default = $this->service->defaultForUser($this->user); expect($default)->not->toBeNull(); expect($default->id)->toBe($this->workspaceNamespace->id); }); it('returns null when user has no accessible namespaces', function () { $lonelyUser = User::factory()->create(['name' => 'Lonely User']); $default = $this->service->defaultForUser($lonelyUser); expect($default)->toBeNull(); }); it('skips inactive namespaces when resolving default', function () { $this->userNamespace->update(['is_active' => false]); $default = $this->service->defaultForUser($this->user); // Should skip the inactive personal namespace and fall back to workspace ns expect($default)->not->toBeNull(); expect($default->id)->toBe($this->workspaceNamespace->id); }); it('returns null for defaultForCurrentUser when unauthenticated', function () { $default = $this->service->defaultForCurrentUser(); expect($default)->toBeNull(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Access control // ───────────────────────────────────────────────────────────────────────────── describe('access control', function () { it('grants access to user-owned namespace', function () { $this->actingAs($this->user); expect($this->service->canAccess($this->userNamespace))->toBeTrue(); }); it('grants access to namespace owned by user workspace', function () { $this->actingAs($this->user); expect($this->service->canAccess($this->workspaceNamespace))->toBeTrue(); }); it('denies access to namespace owned by another user', function () { $this->actingAs($this->otherUser); expect($this->service->canAccess($this->userNamespace))->toBeFalse(); }); it('denies access to namespace from workspace user is not member of', function () { $this->actingAs($this->otherUser); expect($this->service->canAccess($this->workspaceNamespace))->toBeFalse(); }); it('denies access when no user is authenticated', function () { expect($this->service->canAccess($this->userNamespace))->toBeFalse(); }); it('grants access to workspace member for workspace namespace', function () { // Add otherUser as a member of the first workspace $this->otherUser->hostWorkspaces()->attach($this->workspace, [ 'role' => 'member', 'is_default' => false, ]); $this->actingAs($this->otherUser); expect($this->service->canAccess($this->workspaceNamespace))->toBeTrue(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Collection methods (ownedBy, accessibleBy) // ───────────────────────────────────────────────────────────────────────────── describe('namespace collections', function () { it('returns namespaces owned by user', function () { $namespaces = $this->service->ownedByUser($this->user); expect($namespaces)->toHaveCount(1); expect($namespaces->first()->id)->toBe($this->userNamespace->id); }); it('returns namespaces owned by workspace', function () { $namespaces = $this->service->ownedByWorkspace($this->workspace); expect($namespaces)->toHaveCount(1); expect($namespaces->first()->id)->toBe($this->workspaceNamespace->id); }); it('returns all accessible namespaces for user', function () { $namespaces = $this->service->accessibleByUser($this->user); // User owns one namespace and is member of one workspace with a namespace expect($namespaces)->toHaveCount(2); }); it('excludes inactive namespaces from accessible list', function () { $this->workspaceNamespace->update(['is_active' => false]); Cache::flush(); // Clear cached accessible namespaces $namespaces = $this->service->accessibleByUser($this->user); expect($namespaces)->toHaveCount(1); expect($namespaces->first()->id)->toBe($this->userNamespace->id); }); it('returns empty collection for accessibleByCurrentUser when unauthenticated', function () { $namespaces = $this->service->accessibleByCurrentUser(); expect($namespaces)->toBeEmpty(); }); it('caches accessible namespaces for user', function () { $this->service->accessibleByUser($this->user); expect(Cache::has("user:{$this->user->id}:accessible_namespaces"))->toBeTrue(); }); }); // ───────────────────────────────────────────────────────────────────────────── // groupedForUser // ───────────────────────────────────────────────────────────────────────────── describe('grouped namespaces for user', function () { it('separates personal and workspace namespaces', function () { $grouped = $this->service->groupedForUser($this->user); expect($grouped)->toHaveKeys(['personal', 'workspaces']); expect($grouped['personal'])->toHaveCount(1); expect($grouped['personal']->first()->id)->toBe($this->userNamespace->id); expect($grouped['workspaces'])->toHaveCount(1); expect($grouped['workspaces'][0]['workspace']->id)->toBe($this->workspace->id); expect($grouped['workspaces'][0]['namespaces'])->toHaveCount(1); expect($grouped['workspaces'][0]['namespaces']->first()->id)->toBe($this->workspaceNamespace->id); }); it('excludes workspaces with no active namespaces', function () { $this->workspaceNamespace->update(['is_active' => false]); $grouped = $this->service->groupedForUser($this->user); expect($grouped['workspaces'])->toBeEmpty(); }); it('returns empty structure for unauthenticated user via groupedForCurrentUser', function () { $grouped = $this->service->groupedForCurrentUser(); expect($grouped['personal'])->toBeEmpty(); expect($grouped['workspaces'])->toBeEmpty(); }); it('includes multiple workspaces when user is member of several', function () { // Add user to other workspace $this->user->hostWorkspaces()->attach($this->otherWorkspace, [ 'role' => 'member', 'is_default' => false, ]); // Create a namespace in the other workspace Namespace_::create([ 'name' => 'Other Client', 'slug' => 'other-client', 'owner_type' => Workspace::class, 'owner_id' => $this->otherWorkspace->id, 'workspace_id' => $this->otherWorkspace->id, 'is_active' => true, 'sort_order' => 0, ]); $grouped = $this->service->groupedForUser($this->user); expect($grouped['workspaces'])->toHaveCount(2); }); }); // ───────────────────────────────────────────────────────────────────────────── // Cache invalidation // ───────────────────────────────────────────────────────────────────────────── describe('cache invalidation', function () { it('invalidates user accessible namespaces cache', function () { // Populate cache $this->service->accessibleByUser($this->user); expect(Cache::has("user:{$this->user->id}:accessible_namespaces"))->toBeTrue(); // Invalidate $this->service->invalidateUserCache($this->user); expect(Cache::has("user:{$this->user->id}:accessible_namespaces"))->toBeFalse(); }); it('invalidates namespace UUID cache', function () { // Populate cache $this->service->findByUuid($this->userNamespace->uuid); expect(Cache::has("namespace:uuid:{$this->userNamespace->uuid}"))->toBeTrue(); // Invalidate $this->service->invalidateCache($this->userNamespace->uuid); expect(Cache::has("namespace:uuid:{$this->userNamespace->uuid}"))->toBeFalse(); }); });