1
0
Files
breadcrumb-the-shire/tests/Service/Auth/AuthSessionTenantContextServiceTest.php
fs c7b8fd516a feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.

New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering

New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
  module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
  FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00

153 lines
5.7 KiB
PHP

<?php
namespace MintyPHP\Tests\Service\Auth;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\Contracts\SessionProvider;
use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\Http\SessionStore;
use MintyPHP\Service\Auth\AuthSessionTenantContextService;
use MintyPHP\Service\User\UserTenantContextService;
use PHPUnit\Framework\TestCase;
class AuthSessionTenantContextServiceTest extends TestCase
{
protected function setUp(): void
{
$_SESSION = [];
DummyAuthSessionProvider::$populateCalls = 0;
DummyAuthSessionProvider::$clearCalls = 0;
}
public function testHydrateMarksNoActiveTenantWhenUserHasNoTenants(): void
{
$_SESSION['current_tenant'] = ['id' => 9];
$userTenantContextService = $this->createMock(UserTenantContextService::class);
$userTenantContextService->method('getAvailableTenants')->willReturn([]);
$userTenantContextService->expects($this->never())->method('getCurrentTenant');
$service = $this->createService($userTenantContextService);
$service->hydrateForUser(5);
$this->assertSame([], $_SESSION['available_tenants']);
$this->assertTrue((bool) $_SESSION['no_active_tenant']);
$this->assertArrayNotHasKey('current_tenant', $_SESSION);
$this->assertArrayNotHasKey('available_departments_by_tenant', $_SESSION);
}
public function testHydrateUsesFirstAvailableTenantAsFallback(): void
{
$availableTenants = [
['id' => 7, 'uuid' => 'tenant-7'],
['id' => 8, 'uuid' => 'tenant-8'],
];
$userTenantContextService = $this->createMock(UserTenantContextService::class);
$userTenantContextService->method('getAvailableTenants')->willReturn($availableTenants);
$userTenantContextService->method('getCurrentTenant')->willReturn(null);
$service = $this->createService($userTenantContextService);
$service->hydrateForUser(5);
$this->assertSame($availableTenants, $_SESSION['available_tenants']);
$this->assertFalse((bool) $_SESSION['no_active_tenant']);
$this->assertSame(7, (int) ($_SESSION['current_tenant']['id'] ?? 0));
$this->assertArrayNotHasKey('available_departments_by_tenant', $_SESSION);
}
public function testHydratePreservesThemeAndPolicyFieldsInSession(): void
{
$availableTenants = [
[
'id' => 3,
'uuid' => 'tenant-3',
'default_theme' => 'dark',
'allow_user_theme' => '0',
],
];
$userTenantContextService = $this->createMock(UserTenantContextService::class);
$userTenantContextService->method('getAvailableTenants')->willReturn($availableTenants);
$userTenantContextService->method('getCurrentTenant')->willReturn($availableTenants[0]);
$service = $this->createService($userTenantContextService);
$service->hydrateForUser(10);
$tenant = $_SESSION['current_tenant'] ?? [];
$this->assertSame('dark', $tenant['default_theme'] ?? null);
$this->assertSame('0', $tenant['allow_user_theme'] ?? null);
}
public function testHydrateAndClearInvokeConfiguredModuleSessionProviders(): void
{
$fixturesDir = sys_get_temp_dir() . '/auth-session-provider-test-' . uniqid();
mkdir($fixturesDir . '/mod-session', 0777, true);
file_put_contents(
$fixturesDir . '/mod-session/module.php',
'<?php return ' . var_export([
'id' => 'mod-session',
'session_providers' => [DummyAuthSessionProvider::class],
], true) . ';'
);
try {
$registry = ModuleRegistry::boot($fixturesDir, ['mod-session']);
$userTenantContextService = $this->createMock(UserTenantContextService::class);
$userTenantContextService->method('getAvailableTenants')->willReturn([['id' => 7, 'uuid' => 'tenant-7']]);
$userTenantContextService->method('getCurrentTenant')->willReturn(['id' => 7, 'uuid' => 'tenant-7']);
$service = $this->createService($userTenantContextService, $registry);
$_SESSION['user'] = ['id' => 42];
$service->hydrateForUser(42);
$this->assertSame(1, DummyAuthSessionProvider::$populateCalls);
$this->assertSame(42, (int) ($_SESSION['dummy_provider_user_id'] ?? 0));
$service->clearModuleSessionData();
$this->assertSame(1, DummyAuthSessionProvider::$clearCalls);
$this->assertArrayNotHasKey('dummy_provider_user_id', $_SESSION);
} finally {
@unlink($fixturesDir . '/mod-session/module.php');
@rmdir($fixturesDir . '/mod-session');
@rmdir($fixturesDir);
}
}
private function createService(
UserTenantContextService $userTenantContextService,
?ModuleRegistry $registry = null
): AuthSessionTenantContextService {
$registry ??= ModuleRegistry::boot(sys_get_temp_dir() . '/module-registry-empty-' . uniqid(), []);
$container = new AppContainer();
$container->set(ModuleRegistry::class, static fn (): ModuleRegistry => $registry);
return new AuthSessionTenantContextService(
$userTenantContextService,
new SessionStore(),
$container
);
}
}
final class DummyAuthSessionProvider implements SessionProvider
{
public static int $populateCalls = 0;
public static int $clearCalls = 0;
public function populate(array $user, AppContainer $container): void
{
self::$populateCalls++;
$_SESSION['dummy_provider_user_id'] = (int) ($user['id'] ?? 0);
}
public function clear(): void
{
self::$clearCalls++;
unset($_SESSION['dummy_provider_user_id']);
}
}