Compare commits

..

1 Commits

Author SHA1 Message Date
Sebastian 7040681666 chore(deps): update dependency vue-i18n to v11.4.10
Build Test / build (pull_request) Successful in 15s
JS Unit Tests / test (pull_request) Successful in 15s
PHP Integration Tests / Integration Tests (pull_request) Failing after 1m1s
PHP Unit Tests / test (pull_request) Successful in 1m16s
2026-08-31 03:03:11 +00:00
21 changed files with 82 additions and 503 deletions
+5 -10
View File
@@ -6,7 +6,6 @@ use KTXC\Http\Request\Request;
use KTXC\Http\Response\JsonResponse; use KTXC\Http\Response\JsonResponse;
use KTXC\L10N\LocaleResolver; use KTXC\L10N\LocaleResolver;
use KTXC\Module\ModuleManager; use KTXC\Module\ModuleManager;
use KTXC\Module\Configuration\BrowserModuleContext;
use KTXC\Security\Authorization\PermissionChecker; use KTXC\Security\Authorization\PermissionChecker;
use KTXC\Service\UserAccountsService; use KTXC\Service\UserAccountsService;
use KTXC\Context\IdentityContextInterface; use KTXC\Context\IdentityContextInterface;
@@ -31,11 +30,7 @@ class InitController extends ControllerAbstract
$configuration = []; $configuration = [];
// modules - filter by permissions // modules - filter by permissions
$browserContext = new BrowserModuleContext( $configuration['modules'] = [];
$this->tenantContext->requireIdentifier(),
$this->identityContext->requireIdentifier(),
);
foreach ($this->moduleManager->list(true, true) as $module) { foreach ($this->moduleManager->list(true, true) as $module) {
// Check if user has permission to view this module // Check if user has permission to view this module
// Allow access if user has: {module_handle}, {module_handle}.*, or * permission // Allow access if user has: {module_handle}, {module_handle}.*, or * permission
@@ -44,11 +39,11 @@ class InitController extends ControllerAbstract
continue; continue;
} }
$module->configure($browserContext); $integrations = $module->registerBI();
if ($integrations !== null) {
$configuration['modules'][$handle] = $integrations;
}
} }
$configuration['modules'] = $browserContext->modules();
$configuration = array_merge($configuration, $browserContext->configuration());
// localization // localization
$configuration['l10n'] = [ $configuration['l10n'] = [
@@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXC\Module\Configuration;
use InvalidArgumentException;
use LogicException;
use KTXF\Module\Configuration\BrowserModuleContextInterface;
use KTXF\Module\Configuration\ModuleContextType;
use KTXF\Module\ModuleInstanceInterface;
final class BrowserModuleContext implements BrowserModuleContextInterface
{
private const RESERVED_KEYS = ['modules', 'tenant', 'user', 'l10n'];
/** @var array<string,array<string,mixed>> */
private array $modules = [];
/** @var array<string,mixed> */
private array $configuration = [];
public function __construct(
private readonly string $tenantIdentifier,
private readonly string $identityIdentifier,
) {
}
public function type(): ModuleContextType
{
return ModuleContextType::Browser;
}
public function registerModule(
ModuleInstanceInterface $module,
string $namespace,
?string $boot = null,
): void {
if (trim($namespace) === '' || ($boot !== null && trim($boot) === '')) {
throw new InvalidArgumentException('Browser namespace must not be empty and boot path must be null or non-empty');
}
$handle = $module->handle();
if (isset($this->modules[$handle])) {
throw new LogicException("Browser module '{$handle}' is already registered");
}
$entry = [
'handle' => $handle,
'namespace' => $namespace,
'version' => $module->version(),
'label' => $module->label(),
'author' => $module->author(),
'description' => $module->description(),
];
if ($boot !== null) {
$entry['boot'] = $boot;
}
$this->modules[$handle] = $entry;
}
public function set(string $key, mixed $value): void
{
if (trim($key) === '') {
throw new InvalidArgumentException('Browser configuration key must not be empty');
}
if (in_array($key, self::RESERVED_KEYS, true) || array_key_exists($key, $this->configuration)) {
throw new LogicException("Browser configuration '{$key}' is already reserved or registered");
}
$this->configuration[$key] = $value;
}
public function tenantIdentifier(): string
{
return $this->tenantIdentifier;
}
public function identityIdentifier(): string
{
return $this->identityIdentifier;
}
public function modules(): array
{
return $this->modules;
}
public function configuration(): array
{
return $this->configuration;
}
}
@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXC\Module\Configuration;
use InvalidArgumentException;
use KTXF\Module\Configuration\ConsoleModuleContextInterface;
use KTXF\Module\Configuration\ModuleContextType;
final class ConsoleModuleContext implements ConsoleModuleContextInterface
{
/** @var array<class-string,true> */
private array $commands = [];
public function type(): ModuleContextType
{
return ModuleContextType::Console;
}
public function registerCommand(string $command): void
{
if (trim($command) === '') {
throw new InvalidArgumentException('Console command class must not be empty');
}
$this->commands[$command] = true;
}
public function commands(): array
{
return array_keys($this->commands);
}
}
+10 -21
View File
@@ -4,7 +4,6 @@ namespace KTXC\Module;
use KTXC\Console\Firewall\FirewallMaintenanceCommand; use KTXC\Console\Firewall\FirewallMaintenanceCommand;
use KTXC\Console\Firewall\FirewallSetupCommand; use KTXC\Console\Firewall\FirewallSetupCommand;
use KTXC\Preview\PreviewManager;
use KTXC\Service\FirewallService; use KTXC\Service\FirewallService;
use KTXC\Service\SystemFirewallLogService; use KTXC\Service\SystemFirewallLogService;
use KTXC\Service\SystemFirewallRuleService; use KTXC\Service\SystemFirewallRuleService;
@@ -26,9 +25,8 @@ use KTXC\Security\Event\RateLimitExceededEvent;
use KTXC\Security\Event\SuspiciousActivityEvent; use KTXC\Security\Event\SuspiciousActivityEvent;
use KTXF\Event\DeliveryMode; use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistrarInterface; use KTXF\Event\EventListenerRegistrarInterface;
use KTXF\Module\Configuration\BrowserModuleContextInterface; use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\Configuration\ConsoleModuleContextInterface; use KTXF\Module\ModuleConsoleInterface;
use KTXF\Module\Configuration\ModuleContextInterface;
use KTXF\Module\ModuleInstanceAbstract; use KTXF\Module\ModuleInstanceAbstract;
/** /**
@@ -36,11 +34,10 @@ use KTXF\Module\ModuleInstanceAbstract;
* *
* Provides core system functionality and permissions * Provides core system functionality and permissions
*/ */
class Module extends ModuleInstanceAbstract class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, ModuleBrowserInterface
{ {
public function __construct( public function __construct(
private readonly EventListenerRegistrarInterface $events, private readonly EventListenerRegistrarInterface $events,
private readonly PreviewManager $previews,
) { ) {
} }
@@ -216,20 +213,9 @@ class Module extends ModuleInstanceAbstract
]; ];
} }
public function configure(ModuleContextInterface $context): void public function registerCI(): array
{ {
if ($context instanceof BrowserModuleContextInterface) { return [
$context->set(
'preview',
$this->previews->availability($context->tenantIdentifier()),
);
}
if (!$context instanceof ConsoleModuleContextInterface) {
return;
}
foreach ([
FirewallSetupCommand::class, FirewallSetupCommand::class,
FirewallMaintenanceCommand::class, FirewallMaintenanceCommand::class,
\KTXC\Console\Event\EventsDebugCommand::class, \KTXC\Console\Event\EventsDebugCommand::class,
@@ -254,8 +240,11 @@ class Module extends ModuleInstanceAbstract
\KTXC\Console\Role\RoleDeleteCommand::class, \KTXC\Console\Role\RoleDeleteCommand::class,
\KTXC\Console\Role\RoleAssignCommand::class, \KTXC\Console\Role\RoleAssignCommand::class,
\KTXC\Console\Role\RoleRevokeCommand::class, \KTXC\Console\Role\RoleRevokeCommand::class,
] as $command) { ];
$context->registerCommand($command);
} }
public function registerBI(): array
{
return [];
} }
} }
+15 -3
View File
@@ -4,7 +4,8 @@ namespace KTXC\Module;
use JsonSerializable; use JsonSerializable;
use KTXC\Module\Store\ModuleEntry; use KTXC\Module\Store\ModuleEntry;
use KTXF\Module\Configuration\ModuleContextInterface; use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\ModuleConsoleInterface;
use KTXF\Module\ModuleInstanceInterface; use KTXF\Module\ModuleInstanceInterface;
/** /**
@@ -174,9 +175,20 @@ class ModuleObject implements JsonSerializable
$this->instance?->upgrade(); $this->instance?->upgrade();
} }
public function configure(ModuleContextInterface $context): void public function registerBI(): array | null
{ {
$this->instance?->configure($context); if ($this->instance instanceof ModuleBrowserInterface) {
return $this->instance->registerBI();
}
return null;
}
public function registerCI(): array | null
{
if ($this->instance instanceof ModuleConsoleInterface) {
return $this->instance->registerCI();
}
return null;
} }
} }
+5 -39
View File
@@ -29,29 +29,6 @@ final readonly class PreviewManager
) { ) {
} }
/** @return array{enabled:bool,storage:bool,generation:bool} */
public function availability(string $tenantId): array
{
$tenant = $this->tenants->fetchById($tenantId);
if ($tenant === null) {
return ['enabled' => false, 'storage' => false, 'generation' => false];
}
$configuration = $tenant->getConfiguration()->preview();
$enabled = $configuration->enabled();
$storage = $enabled
&& $tenant->getConfiguration()->stores()->store($configuration->store()) !== null;
$generation = $storage && $this->previewProviders() !== [];
if ($enabled && !$storage) {
$this->logger->debug('Preview storage is not configured', ['tenantId' => $tenantId]);
} elseif ($enabled && $storage && !$generation) {
$this->logger->debug('No preview generation provider is available', ['tenantId' => $tenantId]);
}
return compact('enabled', 'storage', 'generation');
}
public function fetch( public function fetch(
string $tenantId, string $tenantId,
PreviewSource $source, PreviewSource $source,
@@ -68,7 +45,6 @@ final readonly class PreviewManager
!$configuration->enabled() !$configuration->enabled()
|| $variantConfiguration === null || $variantConfiguration === null
|| ($source->size !== null && $source->size > $configuration->maxSourceSize()) || ($source->size !== null && $source->size > $configuration->maxSourceSize())
|| $tenant->getConfiguration()->stores()->store($configuration->store()) === null
) { ) {
return null; return null;
} }
@@ -93,9 +69,6 @@ final readonly class PreviewManager
} }
$provider = $this->selectProvider($source->mimeType, $request); $provider = $this->selectProvider($source->mimeType, $request);
if ($provider === null) {
return null;
}
$result = $provider->generate($source, $request); $result = $provider->generate($source, $request);
$blob = $this->writeCache( $blob = $this->writeCache(
$tenantId, $tenantId,
@@ -245,11 +218,11 @@ final readonly class PreviewManager
private function selectProvider( private function selectProvider(
string $sourceMimeType, string $sourceMimeType,
PreviewRequest $request, PreviewRequest $request,
): ?PreviewProviderInterface { ): PreviewProviderInterface {
$sourceMimeType = MimeType::normalize($sourceMimeType); $sourceMimeType = MimeType::normalize($sourceMimeType);
$candidates = []; $candidates = [];
foreach ($this->previewProviders() as $identifier => $provider) { foreach ($this->providers->providers(ProviderInterface::TYPE_PREVIEW) as $identifier => $provider) {
if ( if (
!$provider instanceof PreviewProviderInterface !$provider instanceof PreviewProviderInterface
|| !$provider->supports($sourceMimeType, $request) || !$provider->supports($sourceMimeType, $request)
@@ -261,7 +234,9 @@ final readonly class PreviewManager
} }
if ($candidates === []) { if ($candidates === []) {
return null; throw new PreviewGenerationException(
"No preview provider supports {$sourceMimeType} to {$request->preferredMimeType}"
);
} }
usort($candidates, static function (array $left, array $right): int { usort($candidates, static function (array $left, array $right): int {
@@ -271,13 +246,4 @@ final readonly class PreviewManager
return $candidates[0]['provider']; return $candidates[0]['provider'];
} }
/** @return array<string, PreviewProviderInterface> */
private function previewProviders(): array
{
return array_filter(
$this->providers->providers(ProviderInterface::TYPE_PREVIEW),
static fn(ProviderInterface $provider): bool => $provider instanceof PreviewProviderInterface,
);
}
} }
+6 -4
View File
@@ -7,8 +7,8 @@ namespace KTXC\Runtime\Console;
use KTXC\Application\Execution\ExecutionDescriptor; use KTXC\Application\Execution\ExecutionDescriptor;
use KTXC\Kernel; use KTXC\Kernel;
use KTXC\KernelInterface; use KTXC\KernelInterface;
use KTXC\Module\Configuration\ConsoleModuleContext;
use KTXC\Module\ModuleManager; use KTXC\Module\ModuleManager;
use KTXF\Module\ModuleConsoleInterface;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Application as ConsoleApplication; use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
@@ -34,14 +34,16 @@ final class ConsoleRuntime
/** @var ModuleManager $moduleManager */ /** @var ModuleManager $moduleManager */
$moduleManager = $container->get(ModuleManager::class); $moduleManager = $container->get(ModuleManager::class);
$moduleContext = new ConsoleModuleContext();
foreach ($moduleManager->list() as $module) { foreach ($moduleManager->list() as $module) {
$module->configure($moduleContext); $instance = $module->instance();
if (!$instance instanceof ModuleConsoleInterface) {
continue;
} }
foreach ($moduleContext->commands() as $commandClass) { foreach ($instance->registerCI() as $commandClass) {
$this->registerCommand($console, $container, $commandClass); $this->registerCommand($console, $container, $commandClass);
} }
}
return $console->run($input, $output); return $console->run($input, $output);
} }
-3
View File
@@ -6,7 +6,6 @@ import { createPinia } from 'pinia'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar' import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import { useModuleStore } from '@KTXC/stores/moduleStore' import { useModuleStore } from '@KTXC/stores/moduleStore'
import { useTenantStore } from '@KTXC/stores/tenantStore' import { useTenantStore } from '@KTXC/stores/tenantStore'
import { usePreviewStore } from '@KTXC/stores/previewStore'
import { useUserStore } from '@KTXC/stores/userStore' import { useUserStore } from '@KTXC/stores/userStore'
import { useL10nStore } from '@KTXC/stores/l10nStore' import { useL10nStore } from '@KTXC/stores/l10nStore'
import { useThemeStore } from '@KTXC/stores/themeStore' import { useThemeStore } from '@KTXC/stores/themeStore'
@@ -42,7 +41,6 @@ globalWindow.Pinia = PiniaLib as unknown
(async () => { (async () => {
const moduleStore = useModuleStore(); const moduleStore = useModuleStore();
const tenantStore = useTenantStore(); const tenantStore = useTenantStore();
const previewStore = usePreviewStore();
const userStore = useUserStore(); const userStore = useUserStore();
const l10nStore = useL10nStore(); const l10nStore = useL10nStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@@ -51,7 +49,6 @@ globalWindow.Pinia = PiniaLib as unknown
try { try {
const payload = await fetchWrapper.get('/init'); const payload = await fetchWrapper.get('/init');
moduleStore.init(payload?.modules ?? {}); moduleStore.init(payload?.modules ?? {});
previewStore.init(payload?.preview ?? null);
tenantStore.init(payload?.tenant ?? null); tenantStore.init(payload?.tenant ?? null);
userStore.init(payload?.user ?? {}); userStore.init(payload?.user ?? {});
layoutStore.hydrateFromSettings(); layoutStore.hydrateFromSettings();
-2
View File
@@ -9,8 +9,6 @@
// Stores // Stores
export { useModuleStore } from '../stores/moduleStore' export { useModuleStore } from '../stores/moduleStore'
export { useTenantStore } from '../stores/tenantStore' export { useTenantStore } from '../stores/tenantStore'
export { usePreviewStore } from '../stores/previewStore'
export type { PreviewAvailability } from '../stores/previewStore'
export { useUserStore } from '../stores/userStore' export { useUserStore } from '../stores/userStore'
export { useIntegrationStore } from '../stores/integrationStore' export { useIntegrationStore } from '../stores/integrationStore'
export { useLayoutStore } from '../stores/layoutStore' export { useLayoutStore } from '../stores/layoutStore'
-36
View File
@@ -1,36 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface PreviewAvailability {
enabled: boolean
storage: boolean
generation: boolean
}
const unavailable = (): PreviewAvailability => ({
enabled: false,
storage: false,
generation: false,
})
export const usePreviewStore = defineStore('previewStore', () => {
const availability = ref<PreviewAvailability>(unavailable())
function init(data?: Partial<PreviewAvailability> | null): void {
availability.value = {
enabled: data?.enabled ?? false,
storage: data?.storage ?? false,
generation: data?.generation ?? false,
}
}
function reset(): void {
availability.value = unavailable()
}
return {
availability,
init,
reset,
}
})
@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXF\Module\Configuration;
use KTXF\Module\ModuleInstanceInterface;
interface BrowserModuleContextInterface extends ModuleContextInterface
{
public function registerModule(
ModuleInstanceInterface $module,
string $namespace,
?string $boot = null,
): void;
public function set(string $key, mixed $value): void;
public function tenantIdentifier(): string;
public function identityIdentifier(): string;
/** @return array<string,array<string,mixed>> */
public function modules(): array;
/** @return array<string,mixed> */
public function configuration(): array;
}
@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXF\Module\Configuration;
interface ConsoleModuleContextInterface extends ModuleContextInterface
{
/** @param class-string $command */
public function registerCommand(string $command): void;
/** @return list<class-string> */
public function commands(): array;
}
@@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXF\Module\Configuration;
interface ModuleContextInterface
{
public function type(): ModuleContextType;
}
@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXF\Module\Configuration;
enum ModuleContextType: string
{
case Browser = 'browser';
case Console = 'console';
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace KTXF\Module;
/**
* Module Browser Interface
*/
interface ModuleBrowserInterface
{
public function registerBI(): array;
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace KTXF\Module;
/**
* Module Console Interface
*/
interface ModuleConsoleInterface
{
public function registerCI(): array;
}
@@ -2,8 +2,6 @@
namespace KTXF\Module; namespace KTXF\Module;
use KTXF\Module\Configuration\ModuleContextInterface;
abstract class ModuleInstanceAbstract implements ModuleInstanceInterface abstract class ModuleInstanceAbstract implements ModuleInstanceInterface
{ {
// mandatory methods that must be implemented by each concrete module // mandatory methods that must be implemented by each concrete module
@@ -48,11 +46,6 @@ abstract class ModuleInstanceAbstract implements ModuleInstanceInterface
// Override in specific modules if needed // Override in specific modules if needed
} }
public function configure(ModuleContextInterface $context): void
{
// Override when the module supplies browser or console integrations.
}
/** /**
* Permissions provided by this module * Permissions provided by this module
* *
@@ -2,15 +2,8 @@
namespace KTXF\Module; namespace KTXF\Module;
use KTXF\Module\Configuration\ModuleContextInterface;
interface ModuleInstanceInterface interface ModuleInstanceInterface
{ {
/**
* Declare the integrations supplied by this module.
*/
public function configure(ModuleContextInterface $context): void;
/** /**
* Get module version * Get module version
*/ */
+6 -48
View File
@@ -8,10 +8,6 @@ use KTXC\Console\Firewall\FirewallMaintenanceCommand;
use KTXC\Console\Firewall\FirewallSetupCommand; use KTXC\Console\Firewall\FirewallSetupCommand;
use KTXC\Console\Event\EventsDebugCommand; use KTXC\Console\Event\EventsDebugCommand;
use KTXC\Module\Module; use KTXC\Module\Module;
use KTXC\Module\Configuration\BrowserModuleContext;
use KTXC\Module\Configuration\ConsoleModuleContext;
use KTXC\Preview\PreviewManager;
use KTXC\Resource\ProviderManager;
use KTXC\Service\FirewallService; use KTXC\Service\FirewallService;
use KTXC\Service\SystemFirewallRuleService; use KTXC\Service\SystemFirewallRuleService;
use KTXC\Service\SystemFirewallLogService; use KTXC\Service\SystemFirewallLogService;
@@ -19,8 +15,6 @@ use KTXC\Service\TenantFirewallLogService;
use KTXC\Service\TenantFirewallStatusService; use KTXC\Service\TenantFirewallStatusService;
use KTXC\Service\SystemFirewallStatusService; use KTXC\Service\SystemFirewallStatusService;
use KTXC\Service\TenantFirewallRuleService; use KTXC\Service\TenantFirewallRuleService;
use KTXC\Service\TenantService;
use KTXC\SystemStore\SystemStoreManager;
use KTXC\Event\EventListenerRegistry; use KTXC\Event\EventListenerRegistry;
use KTXC\Security\Event\AccessDeniedEvent; use KTXC\Security\Event\AccessDeniedEvent;
use KTXC\Security\Event\AuthenticationFailedEvent; use KTXC\Security\Event\AuthenticationFailedEvent;
@@ -38,8 +32,6 @@ use KTXF\Event\DeliveryMode;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
final class CoreModuleTest extends TestCase final class CoreModuleTest extends TestCase
{ {
@@ -48,7 +40,7 @@ final class CoreModuleTest extends TestCase
public function registersListeners(): void public function registersListeners(): void
{ {
$registry = new EventListenerRegistry(); $registry = new EventListenerRegistry();
$module = $this->module($registry); $module = new Module($registry);
$module->boot(); $module->boot();
$definitions = $registry->definitions(); $definitions = $registry->definitions();
@@ -91,21 +83,18 @@ final class CoreModuleTest extends TestCase
#[TestDox('Core exposes the event registry debug command')] #[TestDox('Core exposes the event registry debug command')]
public function exposesDebugCommand(): void public function exposesDebugCommand(): void
{ {
$module = $this->module(); $module = new Module(new EventListenerRegistry());
$context = new ConsoleModuleContext();
$module->configure($context);
$commands = $context->commands();
self::assertContains(EventsDebugCommand::class, $commands); self::assertContains(EventsDebugCommand::class, $module->registerCI());
self::assertContains(FirewallSetupCommand::class, $commands); self::assertContains(FirewallSetupCommand::class, $module->registerCI());
self::assertContains(FirewallMaintenanceCommand::class, $commands); self::assertContains(FirewallMaintenanceCommand::class, $module->registerCI());
} }
#[Test] #[Test]
#[TestDox('Core registers dedicated system firewall permissions')] #[TestDox('Core registers dedicated system firewall permissions')]
public function registersSystemFirewallPermissions(): void public function registersSystemFirewallPermissions(): void
{ {
$permissions = $this->module()->permissions(); $permissions = (new Module(new EventListenerRegistry()))->permissions();
self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_READ, $permissions); self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_READ, $permissions);
self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_MANAGE, $permissions); self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_MANAGE, $permissions);
@@ -118,35 +107,4 @@ final class CoreModuleTest extends TestCase
self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ, $permissions); self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ, $permissions);
self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_SETTINGS_MANAGE, $permissions); self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_SETTINGS_MANAGE, $permissions);
} }
#[Test]
#[TestDox('Core resolves preview browser configuration')]
public function resolvesPreviewBrowserConfiguration(): void
{
$module = $this->module();
$context = new BrowserModuleContext('tenant-id', 'identity-id');
$module->configure($context);
self::assertSame([
'preview' => [
'enabled' => false,
'storage' => false,
'generation' => false,
],
], $context->configuration());
}
private function module(?EventListenerRegistry $registry = null): Module
{
$tenants = $this->createStub(TenantService::class);
$providers = new ProviderManager($this->createStub(ContainerInterface::class));
$previews = new PreviewManager(
$tenants,
$providers,
new SystemStoreManager($tenants, $providers),
$this->createStub(LoggerInterface::class),
);
return new Module($registry ?? new EventListenerRegistry(), $previews);
}
} }
@@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXT\Unit\Module;
use KTXC\Module\Configuration\BrowserModuleContext;
use KTXC\Module\Configuration\ConsoleModuleContext;
use KTXF\Module\Configuration\BrowserModuleContextInterface;
use KTXF\Module\Configuration\ConsoleModuleContextInterface;
use KTXF\Module\Configuration\ModuleContextInterface;
use KTXF\Module\Configuration\ModuleContextType;
use KTXF\Module\ModuleInstanceAbstract;
use LogicException;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ModuleContextTest extends TestCase
{
#[Test]
public function moduleRegistersItselfWithTheActiveContext(): void
{
$module = new TestContextModule();
$browser = new BrowserModuleContext('tenant-id', 'identity-id');
$console = new ConsoleModuleContext();
$module->configure($browser);
$module->configure($console);
self::assertSame(
[
'test_context' => [
'handle' => 'test_context',
'namespace' => 'TestContext',
'version' => '0.0.1',
'label' => 'Test Context',
'author' => 'Ktrix',
'description' => 'Tests module contexts',
'boot' => 'static/module.mjs',
],
],
$browser->modules(),
);
self::assertSame(
['test' => ['tenant' => 'tenant-id', 'identity' => 'identity-id']],
$browser->configuration(),
);
self::assertSame(['ExampleCommand'], $console->commands());
self::assertSame(ModuleContextType::Browser, $browser->type());
self::assertSame(ModuleContextType::Console, $console->type());
}
#[Test]
public function rejectsDuplicateBrowserConfigurationKeys(): void
{
$context = new BrowserModuleContext('tenant-id', 'identity-id');
$context->set('test', true);
$this->expectException(LogicException::class);
$context->set('test', false);
}
}
final class TestContextModule extends ModuleInstanceAbstract
{
public function handle(): string { return 'test_context'; }
public function version(): string { return '0.0.1'; }
public function label(): string { return 'Test Context'; }
public function description(): string { return 'Tests module contexts'; }
public function author(): string { return 'Ktrix'; }
public function configure(ModuleContextInterface $context): void
{
if ($context instanceof BrowserModuleContextInterface) {
$context->registerModule($this, 'TestContext', 'static/module.mjs');
$context->set('test', [
'tenant' => $context->tenantIdentifier(),
'identity' => $context->identityIdentifier(),
]);
}
if ($context instanceof ConsoleModuleContextInterface) {
$context->registerCommand('ExampleCommand');
$context->registerCommand('ExampleCommand');
}
}
}
+6 -45
View File
@@ -154,7 +154,12 @@ final class PreviewManagerTest extends TestCase
$service = $this->createMock(SystemStoreServiceInterface::class); $service = $this->createMock(SystemStoreServiceInterface::class);
$service->expects(self::once())->method('stat')->willReturn(null); $service->expects(self::once())->method('stat')->willReturn(null);
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$logger->expects(self::never())->method('warning'); $logger->expects(self::once())->method('warning')->with(
'Preview unavailable',
self::callback(static fn(array $context): bool =>
($context['exception'] ?? null) instanceof PreviewGenerationException
),
);
self::assertNull($this->manager( self::assertNull($this->manager(
$provider, $provider,
@@ -164,50 +169,6 @@ final class PreviewManagerTest extends TestCase
)->fetch('tenant-a', $this->source())); )->fetch('tenant-a', $this->source()));
} }
#[Test]
public function missingPreviewStorageIsUnavailableWithoutLoggingOrProviderLookup(): void
{
$provider = $this->provider();
$provider->expects(self::never())->method('generate');
$providerManager = $this->createMock(ProviderManager::class);
$providerManager->expects(self::never())->method('providers');
$service = $this->createMock(SystemStoreServiceInterface::class);
$service->expects(self::never())->method('stat');
$logger = $this->createMock(LoggerInterface::class);
$logger->expects(self::never())->method('warning');
self::assertNull($this->manager(
$provider,
$service,
new TenantConfiguration(),
$providerManager,
$logger,
)->fetch('tenant-a', $this->source()));
}
#[Test]
public function reportsStorageAndGenerationAvailabilitySeparately(): void
{
$provider = $this->provider();
$providerManager = $this->createMock(ProviderManager::class);
$providerManager->expects(self::once())->method('providers')->willReturn([]);
$logger = $this->createMock(LoggerInterface::class);
$logger->expects(self::once())->method('debug')->with(
'No preview generation provider is available',
['tenantId' => 'tenant-a'],
);
self::assertSame(
['enabled' => true, 'storage' => true, 'generation' => false],
$this->manager(
$provider,
$this->createStub(SystemStoreServiceInterface::class),
providerManager: $providerManager,
logger: $logger,
)->availability('tenant-a'),
);
}
#[Test] #[Test]
public function cacheKeysAreDeterministicOpaqueAndChangeWithSourceIdentity(): void public function cacheKeysAreDeterministicOpaqueAndChangeWithSourceIdentity(): void
{ {