test: add comprehensive tests for NamespaceService
Cover current() resolution (request attributes, session, fallback), session management (set/get/clear), findByUuid/findBySlug lookups, defaultForUser priority chain, access control (owner/member/deny), collection methods (ownedBy, accessibleBy), groupedForUser grouping, and cache invalidation. 40 test cases using Pest syntax. Fixes #30 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c51e4310b1
commit
dff55fef13
1 changed files with 457 additions and 0 deletions
457
tests/Feature/NamespaceServiceTest.php
Normal file
457
tests/Feature/NamespaceServiceTest.php
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Tenant\Models\Namespace_;
|
||||
use Core\Tenant\Models\User;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Core\Tenant\Services\NamespaceService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Cache::flush();
|
||||
|
||||
$this->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();
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue