refactor: align addressbook module to MintyPHP\Module\AddressBook namespace

Migrates addressbook service classes from core namespace
(MintyPHP\Service\AddressBook\*) to proper module namespace
(MintyPHP\Module\AddressBook\Service\*), consistent with the
bookmarks module pattern.

Moved to module:
- AddressBookService → modules/addressbook/lib/Module/AddressBook/Service/
- AddressBookServicesFactory → modules/addressbook/lib/Module/AddressBook/Service/
- AddressBookServiceTest → modules/addressbook/tests/Module/AddressBook/Service/
- Pages, templates, CSS, JS all in module directory

All module PHP classes now live under MintyPHP\Module\<Name>\*,
enforced by ModuleStructureContractTest::testModuleClassesUseModuleNamespace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 22:20:54 +01:00
parent 4871c6032e
commit c328067aa6
24 changed files with 373 additions and 114 deletions

View File

@@ -14,9 +14,7 @@ use FilesystemIterator;
* Intentionally allowed in Core (not flagged):
* - PermissionService::ADDRESS_BOOK_VIEW (general people-visibility gate)
* - OperationsAuthorizationPolicy / UiCapabilityMap / UserAuthorizationPolicy (authorization)
* - AddressBookService / AddressBookServicesFactory (business logic, V1 shared-db)
* - pages/address-book/* (page files, not hardcoded integrations)
* - config/assets.php (asset pipeline not yet modular)
* - modules/addressbook/* (module-owned runtime, pages/assets/providers)
* - i18n translation files
* - Module directory (lib/Module/, modules/)
* - Test files
@@ -90,10 +88,7 @@ final class NoAddressBookHardcodingTest extends TestCase
}
/**
* Aside panel template must not have hardcoded People panel.
*
* Note: The search panel legitimately contains data-search-key="address-book"
* for the global search results — that is NOT a hardcoded People panel.
* Aside panel template must not have hardcoded People panel/search entries.
*/
public function testAsideHasNoHardcodedPeoplePanel(): void
{
@@ -116,6 +111,20 @@ final class NoAddressBookHardcodingTest extends TestCase
$content,
'Aside still defines $peopleGroups as a standalone variable'
);
self::assertStringNotContainsString(
'data-search-key="address-book"',
$content,
'Aside still has static address-book search item markup'
);
}
public function testCoreNoLongerContainsAddressBookServiceDirectory(): void
{
$coreServiceDir = dirname(__DIR__, 2) . '/lib/Service/AddressBook';
self::assertDirectoryDoesNotExist(
$coreServiceDir,
'Addressbook service classes should live in modules/addressbook/lib, not in Core lib/Service'
);
}
/**

View File

@@ -1,89 +0,0 @@
<?php
namespace MintyPHP\Tests\Service\AddressBook;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\AddressBook\AddressBookService;
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserAssignmentService;
use MintyPHP\Service\User\UserAvatarService;
use PHPUnit\Framework\TestCase;
class AddressBookServiceTest extends TestCase
{
public function testBuildIndexContextBuildsTenantDepartmentMapForGlobalScope(): void
{
$userAccountService = $this->createMock(UserAccountService::class);
$userAssignmentService = $this->createMock(UserAssignmentService::class);
$tenantService = $this->createMock(TenantService::class);
$departmentService = $this->createMock(DepartmentService::class);
$roleService = $this->createMock(RoleService::class);
$scopeGateway = $this->createMock(TenantScopeService::class);
$customFieldGateway = $this->createMock(UserCustomFieldValueService::class);
$avatarService = $this->createMock(UserAvatarService::class);
$scopeGateway
->expects($this->once())
->method('getUserTenantIds')
->with(7)
->willReturn([]);
$tenantService
->expects($this->once())
->method('list')
->willReturn([
['id' => 1, 'uuid' => 'tenant-a', 'description' => 'Tenant A'],
['id' => 2, 'uuid' => 'tenant-b', 'description' => 'Tenant B'],
]);
$scopeGateway
->expects($this->exactly(2))
->method('isStrict')
->willReturn(false);
$departmentService
->expects($this->once())
->method('list')
->willReturn([
['id' => 10, 'tenant_id' => 1, 'description' => 'Sales'],
['id' => 20, 'tenant_id' => 2, 'description' => 'Support'],
]);
$departmentService
->expects($this->never())
->method('listByTenantIds');
$roleService
->expects($this->once())
->method('listActive')
->willReturn([]);
$customFieldGateway
->expects($this->once())
->method('extractAddressBookFilterSpec')
->with([], 7)
->willReturn([
'definitions' => [],
'query' => [],
]);
$service = new AddressBookService(
$userAccountService,
$userAssignmentService,
$tenantService,
$departmentService,
$roleService,
$scopeGateway,
$customFieldGateway,
$avatarService
);
$context = $service->buildIndexContext(7, []);
$this->assertSame(
[
'tenant-a' => ['10'],
'tenant-b' => ['20'],
],
$context['tenantDepartmentMap']
);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace MintyPHP\Tests\Unit\Module;
use MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy;
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Tests\Service\Access\AuthorizationPolicyTestSupport;
use PHPUnit\Framework\TestCase;
/**
* @covers \MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy
*/
final class AddressBookAuthorizationPolicyTest extends TestCase
{
use AuthorizationPolicyTestSupport;
public function testImplementsInterface(): void
{
$policy = new AddressBookAuthorizationPolicy($this->createMock(PermissionService::class));
self::assertInstanceOf(AuthorizationPolicyInterface::class, $policy);
}
public function testSupportsOwnAbility(): void
{
$policy = new AddressBookAuthorizationPolicy($this->createMock(PermissionService::class));
self::assertTrue($policy->supports(AddressBookAuthorizationPolicy::ABILITY_VIEW));
}
public function testDoesNotSupportOtherAbilities(): void
{
$policy = new AddressBookAuthorizationPolicy($this->createMock(PermissionService::class));
self::assertFalse($policy->supports('ops.admin.jobs.view'));
self::assertFalse($policy->supports(''));
self::assertFalse($policy->supports('ops.address_book.view')); // old ability string
}
public function testAllowedWithPermission(): void
{
$permissionService = $this->permissionGatewayAllowing([
5 => [PermissionService::ADDRESS_BOOK_VIEW],
]);
$policy = new AddressBookAuthorizationPolicy($permissionService);
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, [
'actor_user_id' => 5,
]);
self::assertTrue($decision->isAllowed());
}
public function testDeniedWithoutPermission(): void
{
$permissionService = $this->permissionGatewayAllowing([5 => []]);
$policy = new AddressBookAuthorizationPolicy($permissionService);
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, [
'actor_user_id' => 5,
]);
$this->assertForbiddenDecision($decision);
}
public function testDeniedWithZeroActorId(): void
{
$permissionService = $this->createMock(PermissionService::class);
$permissionService->expects($this->never())->method('userHas');
$policy = new AddressBookAuthorizationPolicy($permissionService);
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, [
'actor_user_id' => 0,
]);
$this->assertForbiddenDecision($decision);
}
public function testDeniedWithMissingActorId(): void
{
$permissionService = $this->createMock(PermissionService::class);
$permissionService->expects($this->never())->method('userHas');
$policy = new AddressBookAuthorizationPolicy($permissionService);
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, []);
$this->assertForbiddenDecision($decision);
}
}