1
0
Files
breadcrumb-the-shire/tests/Service/Tenant/TenantServiceTest.php
fs 68453fd6ed refactor(tenants): visibility form requires explicit appearance values
Simplifies the tenant Visibility tab to match the tenant-only appearance
model established by the previous commits:

- primary_color: now required. Removed the "No brand color / Use system
  default (no brand accent)" toggle — every tenant carries a concrete
  primary color. Legacy NULL rows render the neutral app default (#2fa4a4)
  in the color picker and are converted to that explicit hex on save.
- default_theme: the select no longer offers a "Use system default" empty
  option. Legacy NULL rows resolve to 'light' at render time; saves always
  persist a valid theme via SettingsAppGateway::normalizeTheme().
- allow_user_theme: replaced the tri-state "Use system default (allowed) /
  Force allow / Force disallow" select with a single boolean switch
  ("Users may choose their own theme"). Legacy NULL rows load as checked.
  Saves persist 0/1 explicitly.

TenantService: sanitize no longer reads primary_color_use_default or
allow_user_theme_mode; it validates primary_color as a required hex and
treats allow_user_theme as a plain boolean. Both create and update paths
write concrete values only — no more NULL writes for these three fields.

DirectorySettingsGateway gains a normalizeTheme() delegate so TenantService
can route through the same gateway it uses for isAllowedTheme().

Removed now-unused app-color-default-toggle JS component + its runtime
registration + its architecture-test entry. i18n cleanup: "No brand
color", "Use system default (no brand accent)", "When enabled the tenant
renders without a brand accent color.", "Use system default (light)",
"Use system default (allowed)", "Force allow user theme", "Force disallow
user theme", "User theme policy is invalid" all removed. New copy: "Users
may choose their own theme" + helper text, plus a tightened tab blockquote.

Tests: TenantServiceTest validInput() updated to send concrete values;
settingsGateway mock gets normalizeTheme() + isAllowedTheme() defaults.
All 1985 tests pass; PHPStan level 5 clean; QG-006 clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:10:44 +02:00

208 lines
6.6 KiB
PHP

<?php
namespace MintyPHP\Tests\Service\Tenant;
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Directory\DirectorySettingsGateway;
use MintyPHP\Service\Tenant\TenantService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class TenantServiceTest extends TestCase
{
private TenantRepositoryInterface&MockObject $tenantRepository;
private DepartmentRepositoryInterface&MockObject $departmentRepository;
private DirectorySettingsGateway&MockObject $settingsGateway;
private AuditRecorderInterface&MockObject $systemAuditService;
private TenantService $service;
protected function setUp(): void
{
$this->tenantRepository = $this->createMock(TenantRepositoryInterface::class);
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
$this->settingsGateway->method('isAllowedTheme')->willReturn(true);
$this->settingsGateway->method('normalizeTheme')
->willReturnCallback(static fn (?string $t): string => $t ?? 'light');
$this->systemAuditService = $this->createMock(AuditRecorderInterface::class);
$this->service = new TenantService(
$this->tenantRepository,
$this->departmentRepository,
$this->settingsGateway,
$this->systemAuditService
);
}
public function testCreateFromAdminSucceeds(): void
{
$input = $this->validInput();
$this->tenantRepository->expects($this->once())
->method('create')
->willReturn(42);
$this->tenantRepository->expects($this->once())
->method('find')
->with(42)
->willReturn(['id' => 42, 'uuid' => 'abc-uuid-123']);
$this->systemAuditService->expects($this->once())
->method('record');
$result = $this->service->createFromAdmin($input, 5);
self::assertTrue($result['ok']);
self::assertSame('abc-uuid-123', $result['uuid']);
self::assertSame(42, $result['id']);
self::assertSame('Test Tenant', $result['form']['description']);
}
public function testCreateFromAdminRejectsEmptyDescription(): void
{
$input = $this->validInput();
$input['description'] = '';
$this->tenantRepository->expects($this->never())
->method('create');
$result = $this->service->createFromAdmin($input);
self::assertFalse($result['ok']);
self::assertNotEmpty($result['errors']);
}
public function testUpdateFromAdminSucceeds(): void
{
$input = $this->validInput();
$this->tenantRepository->expects($this->once())
->method('find')
->with(10)
->willReturn(['id' => 10, 'uuid' => 'existing-uuid', 'status' => 'active']);
$this->tenantRepository->expects($this->once())
->method('update')
->with(10, $this->isArray())
->willReturn(true);
$this->systemAuditService->expects($this->once())
->method('record');
$result = $this->service->updateFromAdmin(10, $input, 5);
self::assertTrue($result['ok']);
self::assertSame('Test Tenant', $result['form']['description']);
}
public function testDeleteByUuidSucceeds(): void
{
$tenant = ['id' => 7, 'uuid' => 'del-uuid-789'];
$this->tenantRepository->expects($this->once())
->method('findByUuid')
->with('del-uuid-789')
->willReturn($tenant);
$this->departmentRepository->expects($this->once())
->method('countByTenantId')
->with(7)
->willReturn(0);
$this->tenantRepository->expects($this->once())
->method('delete')
->with(7)
->willReturn(true);
$this->systemAuditService->expects($this->once())
->method('record');
$result = $this->service->deleteByUuid('del-uuid-789');
self::assertTrue($result['ok']);
self::assertSame($tenant, $result['tenant']);
}
public function testDeleteByUuidBlocksWhenDepartmentsExist(): void
{
$tenant = ['id' => 7, 'uuid' => 'dep-uuid-789'];
$this->tenantRepository->expects($this->once())
->method('findByUuid')
->with('dep-uuid-789')
->willReturn($tenant);
$this->departmentRepository->expects($this->once())
->method('countByTenantId')
->with(7)
->willReturn(3);
$this->tenantRepository->expects($this->never())
->method('delete');
$result = $this->service->deleteByUuid('dep-uuid-789');
self::assertFalse($result['ok']);
self::assertSame(409, $result['status']);
self::assertSame('tenant_has_departments', $result['error']);
}
public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void
{
$this->tenantRepository->expects($this->never())
->method('findByUuid');
$result = $this->service->deleteByUuid('');
self::assertFalse($result['ok']);
self::assertSame(404, $result['status']);
self::assertSame('not_found', $result['error']);
}
public function testDeleteByUuidReturnsNotFoundForMissingTenant(): void
{
$this->tenantRepository->expects($this->once())
->method('findByUuid')
->with('nonexistent-uuid')
->willReturn(null);
$result = $this->service->deleteByUuid('nonexistent-uuid');
self::assertFalse($result['ok']);
self::assertSame(404, $result['status']);
self::assertSame('not_found', $result['error']);
}
/**
* @return array<string, mixed>
*/
private function validInput(): array
{
return [
'description' => 'Test Tenant',
'status' => 'active',
'address' => '',
'postal_code' => '',
'city' => '',
'country' => '',
'region' => '',
'vat_id' => '',
'tax_number' => '',
'phone' => '',
'fax' => '',
'email' => '',
'support_email' => '',
'support_phone' => '',
'billing_email' => '',
'website' => '',
'privacy_url' => '',
'imprint_url' => '',
'primary_color' => '#2fa4a4',
'default_theme' => 'light',
'allow_user_theme' => '1',
];
}
}