toBe('customer:read'); expect(scope_registry::CUSTOMER_WRITE)->toBe('customer:write'); expect(scope_registry::BOOKING_READ)->toBe('booking:read'); expect(scope_registry::BOOKING_WRITE)->toBe('booking:write'); expect(scope_registry::SUBUSER_READ)->toBe('subuser:read'); expect(scope_registry::SUBUSER_WRITE)->toBe('subuser:write'); expect(scope_registry::INVOICE_READ)->toBe('invoice:read'); expect(scope_registry::INVOICE_WRITE)->toBe('invoice:write'); expect(scope_registry::SUPERUSER_READ)->toBe('superuser:read'); expect(scope_registry::SUPERUSER_WRITE)->toBe('superuser:write'); }); it('returns every concrete scope from all()', function (): void { $all = scope_registry::all(); expect($all)->toContain(scope_registry::CUSTOMER_READ); expect($all)->toContain(scope_registry::CUSTOMER_WRITE); expect($all)->toContain(scope_registry::BOOKING_READ); expect($all)->toContain(scope_registry::BOOKING_WRITE); expect($all)->toContain(scope_registry::SUBUSER_READ); expect(scope_registry::SUBUSER_WRITE); expect($all)->toContain(scope_registry::INVOICE_READ); expect($all)->toContain(scope_registry::INVOICE_WRITE); expect($all)->toContain(scope_registry::SUPERUSER_READ); expect($all)->toContain(scope_registry::SUPERUSER_WRITE); expect(count($all))->toBe(10); expect(count(array_unique($all)))->toBe(10); }); it('superuser defaults to the global wildcard', function (): void { expect(scope_registry::scopesForRole('superuser'))->toBe(['*']); }); it('admin defaults to all resource wildcards', function (): void { $scopes = scope_registry::scopesForRole('admin'); expect($scopes)->toContain('customer:*'); expect($scopes)->toContain('booking:*'); expect($scopes)->toContain('subuser:*'); expect($scopes)->toContain('invoice:*'); expect($scopes)->not->toContain('superuser:*'); }); it('customer defaults to read-only on self resources', function (): void { $scopes = scope_registry::scopesForRole('customer'); expect($scopes)->toBe([ scope_registry::CUSTOMER_READ, scope_registry::BOOKING_READ, scope_registry::INVOICE_READ, ]); }); it('subuser defaults to booking read+write on assigned bookings', function (): void { $scopes = scope_registry::scopesForRole('subuser'); expect($scopes)->toBe([ scope_registry::BOOKING_READ, scope_registry::BOOKING_WRITE, ]); }); it('unknown roles default to no scopes', function (): void { expect(scope_registry::scopesForRole('nope'))->toBe([]); expect(scope_registry::scopesForRole(''))->toBe([]); expect(scope_registry::scopesForRole('SuperUser'))->toBe(['*']); // case-insensitive }); it('hasScope matches an exact scope against itself', function (): void { expect(scope_registry::hasScope([scope_registry::BOOKING_READ], scope_registry::BOOKING_READ))->toBeTrue(); }); it('hasScope rejects an exact scope against a different scope', function (): void { expect(scope_registry::hasScope([scope_registry::BOOKING_READ], scope_registry::BOOKING_WRITE))->toBeFalse(); }); it('hasScope lets a wildcard match any concrete scope', function (): void { expect(scope_registry::hasScope(['*'], scope_registry::INVOICE_READ))->toBeTrue(); expect(scope_registry::hasScope(['*'], scope_registry::SUPERUSER_WRITE))->toBeTrue(); }); it('hasScope resolves a resource wildcard to that resource only', function (): void { expect(scope_registry::hasScope(['customer:*'], scope_registry::CUSTOMER_READ))->toBeTrue(); expect(scope_registry::hasScope(['customer:*'], scope_registry::CUSTOMER_WRITE))->toBeTrue(); expect(scope_registry::hasScope(['customer:*'], scope_registry::BOOKING_READ))->toBeFalse(); }); it('hasScope returns false on empty input', function (): void { expect(scope_registry::hasScope([], 'booking:read'))->toBeFalse(); expect(scope_registry::hasScope(['booking:read'], ''))->toBeFalse(); }); it('hasScope ignores non-string granted entries', function (): void { expect(scope_registry::hasScope([null, 123, 'booking:read'], 'booking:read'))->toBeTrue(); expect(scope_registry::hasScope([null, 123], 'booking:read'))->toBeFalse(); }); it('expand flattens a single wildcard to all concrete scopes', function (): void { $expanded = scope_registry::expand(['*']); expect(count($expanded))->toBe(10); expect($expanded)->toContain(scope_registry::BOOKING_READ); expect($expanded)->toContain(scope_registry::SUPERUSER_WRITE); }); it('expand flattens resource wildcards', function (): void { $expanded = scope_registry::expand(['booking:*']); expect($expanded)->toBe([ scope_registry::BOOKING_READ, scope_registry::BOOKING_WRITE, ]); }); it('expand deduplicates results', function (): void { $expanded = scope_registry::expand([ 'booking:*', scope_registry::BOOKING_READ, 'booking:write', ]); expect($expanded)->toBe([ scope_registry::BOOKING_READ, scope_registry::BOOKING_WRITE, ]); }); it('expand drops unknown concrete scopes (no silent grant)', function (): void { $expanded = scope_registry::expand(['booking:read', 'totally:made-up']); expect($expanded)->toBe([scope_registry::BOOKING_READ]); }); it('expand combines multiple wildcards and concrete scopes', function (): void { $expanded = scope_registry::expand([ scope_registry::BOOKING_READ, 'customer:*', ]); expect($expanded)->toContain(scope_registry::BOOKING_READ); expect($expanded)->toContain(scope_registry::CUSTOMER_READ); expect($expanded)->toContain(scope_registry::CUSTOMER_WRITE); expect(count($expanded))->toBe(3); }); it('expand ignores empty and non-string entries', function (): void { $expanded = scope_registry::expand([null, '', ' ', scope_registry::BOOKING_READ]); expect($expanded)->toBe([scope_registry::BOOKING_READ]); }); it('superuser role resolves to all scopes via expand', function (): void { $expanded = scope_registry::expand(scope_registry::scopesForRole('superuser')); expect(count($expanded))->toBe(10); }); it('admin role expands to all non-superuser scopes', function (): void { $expanded = scope_registry::expand(scope_registry::scopesForRole('admin')); expect($expanded)->toContain(scope_registry::CUSTOMER_READ); expect($expanded)->toContain(scope_registry::CUSTOMER_WRITE); expect($expanded)->toContain(scope_registry::BOOKING_READ); expect($expanded)->toContain(scope_registry::BOOKING_WRITE); expect($expanded)->toContain(scope_registry::SUBUSER_READ); expect($expanded)->toContain(scope_registry::SUBUSER_WRITE); expect($expanded)->toContain(scope_registry::INVOICE_READ); expect($expanded)->toContain(scope_registry::INVOICE_WRITE); expect($expanded)->not->toContain(scope_registry::SUPERUSER_READ); expect($expanded)->not->toContain(scope_registry::SUPERUSER_WRITE); expect(count($expanded))->toBe(8); }); it('customer role does not gain write or subuser scopes', function (): void { $expanded = scope_registry::expand(scope_registry::scopesForRole('customer')); expect($expanded)->not->toContain(scope_registry::CUSTOMER_WRITE); expect($expanded)->not->toContain(scope_registry::BOOKING_WRITE); expect($expanded)->not->toContain(scope_registry::SUBUSER_READ); expect($expanded)->not->toContain(scope_registry::INVOICE_WRITE); }); it('isValid accepts canonical scopes, wildcards, and resource wildcards', function (): void { expect(scope_registry::isValid('*'))->toBeTrue(); expect(scope_registry::isValid('customer:*'))->toBeTrue(); expect(scope_registry::isValid(scope_registry::BOOKING_READ))->toBeTrue(); expect(scope_registry::isValid('totally:made-up'))->toBeFalse(); expect(scope_registry::isValid(''))->toBeFalse(); expect(scope_registry::isValid(' '))->toBeFalse(); expect(scope_registry::isValid('unknown:*'))->toBeFalse(); }); it('role default + hasScope composes correctly for customer:read on customer role', function (): void { $granted = scope_registry::scopesForRole('customer'); expect(scope_registry::hasScope($granted, scope_registry::CUSTOMER_READ))->toBeTrue(); expect(scope_registry::hasScope($granted, scope_registry::CUSTOMER_WRITE))->toBeFalse(); expect(scope_registry::hasScope($granted, scope_registry::SUBUSER_READ))->toBeFalse(); });