'testmod'], '/tmp/modules/testmod'); self::assertSame('testmod', $manifest->id); self::assertSame('0.0.0', $manifest->version); self::assertFalse($manifest->enabledByDefault); self::assertSame(100, $manifest->loadOrder); self::assertSame([], $manifest->routes); self::assertSame([], $manifest->permissions); self::assertSame([], $manifest->searchResources); self::assertNull($manifest->migrationsPath); } public function testFullManifest(): void { $raw = [ 'id' => 'addressbook', 'version' => '1.0.0', 'enabled_by_default' => true, 'load_order' => 10, 'routes' => [['path' => '/address-book', 'target' => 'address-book/index']], 'public_paths' => [], 'container_registrars' => ['App\\Registrar'], 'ui_slots' => ['aside.tab_panel' => [['key' => 'people', 'label' => 'People']]], 'search_resources' => ['App\\SearchProvider'], 'asset_groups' => ['address-book' => ['css/view.css']], 'scheduler_jobs' => [ [ 'job_key' => 'addressbook.sync', 'handler' => 'App\\Scheduler\\AddressBookSyncHandler', 'label' => 'Address book sync', 'description' => 'Syncs address data', 'default_enabled' => true, 'default_timezone' => 'Europe/Berlin', 'default_schedule_type' => 'daily', 'default_schedule_interval' => 1, 'default_schedule_time' => '02:00', 'default_schedule_weekdays_csv' => null, 'default_catchup_once' => true, 'allowed_schedule_types' => ['daily', 'weekly'], ], ], 'layout_context_providers' => ['App\\LayoutProvider'], 'session_providers' => ['App\\SessionProvider'], 'permissions' => [ [ 'key' => 'address_book.view', 'description' => 'Can view address book', 'active' => true, 'is_system' => true, ], ], 'migrations_path' => 'db/updates', ]; $manifest = ModuleManifest::fromArray($raw, '/srv/modules/addressbook'); self::assertSame('addressbook', $manifest->id); self::assertSame('1.0.0', $manifest->version); self::assertTrue($manifest->enabledByDefault); self::assertSame(10, $manifest->loadOrder); self::assertCount(1, $manifest->routes); self::assertSame([[ 'key' => 'address_book.view', 'description' => 'Can view address book', 'active' => 1, 'is_system' => 1, ]], $manifest->permissions); self::assertSame('addressbook.sync', $manifest->schedulerJobs[0]['job_key']); self::assertSame('/srv/modules/addressbook/db/updates', $manifest->migrationsPath); self::assertSame(['App\\LayoutProvider'], $manifest->layoutContextProviders); self::assertSame(['App\\SessionProvider'], $manifest->sessionProviders); } public function testMissingIdThrows(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('non-empty string "id"'); ModuleManifest::fromArray([], '/tmp/modules/broken'); } public function testEmptyIdThrows(): void { $this->expectException(InvalidArgumentException::class); ModuleManifest::fromArray(['id' => ' '], '/tmp/modules/broken'); } public function testInvalidPermissionContractThrows(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("must define non-empty 'key' and 'description'"); ModuleManifest::fromArray([ 'id' => 'mod-invalid-perm', 'permissions' => [['key' => 'mod.permission']], ], '/tmp/modules/mod-invalid-perm'); } public function testInvalidSchedulerJobContractThrows(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("missing required key 'allowed_schedule_types'"); ModuleManifest::fromArray([ 'id' => 'mod-invalid-job', 'scheduler_jobs' => [[ 'job_key' => 'mod.job', 'handler' => 'App\\Handler', 'label' => 'Label', 'description' => 'Desc', 'default_enabled' => true, 'default_timezone' => 'UTC', 'default_schedule_type' => 'daily', 'default_schedule_interval' => 1, 'default_schedule_time' => '01:00', 'default_schedule_weekdays_csv' => null, 'default_catchup_once' => true, ]], ], '/tmp/modules/mod-invalid-job'); } }