laravel-websockets/tests/Unit/EventRegistryTest.php

316 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace BlaxSoftware\LaravelWebSockets\Tests\Unit;
use BlaxSoftware\LaravelWebSockets\Attributes\Websocket;
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
use BlaxSoftware\LaravelWebSockets\Websocket\EventRegistry;
class EventRegistryTest extends TestCase
{
/**
* Namespace prefix shared by every fixture under tests/Unit/Fixtures/Controllers.
* Tests pin this base namespace explicitly so event-prefix derivation is
* deterministic (independent of where the test runs from).
*/
private const FIXTURES_NS = 'BlaxSoftware\\LaravelWebSockets\\Test\\Unit\\Fixtures\\Controllers';
public function setUp(): void
{
parent::setUp();
EventRegistry::clear();
EventRegistry::setSearchPaths([
__DIR__ . '/Fixtures/Controllers' => self::FIXTURES_NS,
]);
}
public function tearDown(): void
{
EventRegistry::clear();
// Reset to default-detection so other tests aren't affected
EventRegistry::setSearchPaths([
__DIR__ . '/Fixtures/Controllers' => self::FIXTURES_NS,
]);
parent::tearDown();
}
// ─────────────────────────────────────────────────────────────────
// eventPrefixFor() — direct algorithm tests
// ─────────────────────────────────────────────────────────────────
/** @test */
public function it_derives_prefix_for_a_flat_controller()
{
$this->assertSame(
'plain',
EventRegistry::eventPrefixFor(
self::FIXTURES_NS . '\\PlainController',
self::FIXTURES_NS . '\\'
)
);
}
/** @test */
public function it_derives_folder_aware_prefix_for_a_nested_controller()
{
$this->assertSame(
'api-v1-me',
EventRegistry::eventPrefixFor(
self::FIXTURES_NS . '\\Api\\V1\\MeController',
self::FIXTURES_NS . '\\'
)
);
}
/** @test */
public function it_kebabs_multi_word_class_names()
{
$this->assertSame(
'admin-user-settings',
EventRegistry::eventPrefixFor(
'App\\Http\\Controllers\\Admin\\UserSettingsController',
'App\\Http\\Controllers\\'
)
);
}
/** @test */
public function it_falls_back_to_short_name_when_base_namespace_does_not_match()
{
$this->assertSame(
'foo',
EventRegistry::eventPrefixFor(
'Some\\Other\\FooController',
'App\\Http\\Controllers\\'
)
);
}
/** @test */
public function it_strips_the_controller_suffix()
{
$this->assertSame(
'something',
EventRegistry::eventPrefixFor(
'App\\Http\\Controllers\\SomethingController',
'App\\Http\\Controllers\\'
)
);
}
/** @test */
public function it_handles_a_class_named_only_controller_defensively()
{
// 'App\\Http\\Controllers\\Controller' → strip 'Controller' from leaf
// leaves an empty leaf — fall back to a non-empty placeholder.
$prefix = EventRegistry::eventPrefixFor(
'App\\Http\\Controllers\\Controller',
'App\\Http\\Controllers\\'
);
$this->assertNotSame('', $prefix);
}
// ─────────────────────────────────────────────────────────────────
// map() — discovery + auto-defaults
// ─────────────────────────────────────────────────────────────────
/** @test */
public function it_auto_registers_tagged_methods_with_default_event_names()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('plain.alpha', $map);
$this->assertArrayHasKey('plain.bravo', $map);
$this->assertSame(self::FIXTURES_NS . '\\PlainController', $map['plain.alpha']['class']);
$this->assertSame('alpha', $map['plain.alpha']['method']);
$this->assertFalse($map['plain.alpha']['needAuth']);
}
/** @test */
public function it_skips_untagged_methods()
{
$map = EventRegistry::map();
$this->assertArrayNotHasKey('plain.charlie', $map);
}
/** @test */
public function it_uses_folder_aware_prefix_for_nested_namespaces()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('api-v1-me.show', $map);
$this->assertSame(self::FIXTURES_NS . '\\Api\\V1\\MeController', $map['api-v1-me.show']['class']);
$this->assertSame('show', $map['api-v1-me.show']['method']);
$this->assertTrue($map['api-v1-me.show']['needAuth']);
}
/** @test */
public function it_skips_abstract_classes()
{
$map = EventRegistry::map();
// AbstractBaseController is tagged but abstract; nothing should resolve to it
foreach ($map as $event => $target) {
$this->assertStringNotContainsString('AbstractBaseController', $target['class'], "Abstract class leaked via event '{$event}'");
}
}
// ─────────────────────────────────────────────────────────────────
// Override behavior: prefix, suffix, event
// ─────────────────────────────────────────────────────────────────
/** @test */
public function it_uses_default_event_name_when_no_arguments_given()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('override.defaulted', $map);
}
/** @test */
public function it_honors_a_prefix_only_override()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('custom-prefix.prefixed', $map);
$this->assertArrayNotHasKey('override.prefixed', $map, 'Auto-prefix should NOT also register when prefix: is overridden');
$this->assertSame('prefixed', $map['custom-prefix.prefixed']['method']);
}
/** @test */
public function it_honors_a_suffix_only_override()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('override.custom-suffix', $map);
$this->assertArrayNotHasKey('override.suffixed', $map, 'Default method-name suffix should NOT also register when suffix: is overridden');
$this->assertSame('suffixed', $map['override.custom-suffix']['method'], 'PHP method name preserved even when WS suffix differs');
}
/** @test */
public function it_honors_prefix_and_suffix_combined()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('pre.post', $map);
$this->assertSame('bothOverridden', $map['pre.post']['method']);
}
/** @test */
public function the_event_argument_wins_over_prefix_and_suffix()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('totally.custom', $map);
$this->assertArrayNotHasKey('ignored.ignored', $map);
$this->assertArrayNotHasKey('ignored.fullOverride', $map);
$this->assertSame('fullOverride', $map['totally.custom']['method']);
}
// ─────────────────────────────────────────────────────────────────
// Class-level attribute
// ─────────────────────────────────────────────────────────────────
/** @test */
public function class_level_attribute_applies_to_every_public_method()
{
$map = EventRegistry::map();
$this->assertArrayHasKey('class-prefixed.alpha', $map);
$this->assertArrayHasKey('class-prefixed.bravo', $map);
}
/** @test */
public function class_level_need_auth_propagates_to_every_method()
{
$map = EventRegistry::map();
$this->assertTrue($map['class-prefixed.alpha']['needAuth']);
$this->assertTrue($map['class-prefixed.bravo']['needAuth']);
}
/** @test */
public function method_level_attribute_overrides_class_level_for_that_method_only()
{
$map = EventRegistry::map();
// Only the suffix differs — prefix is inherited from class-level
$this->assertArrayHasKey('class-prefixed.remapped', $map);
$this->assertArrayNotHasKey('class-prefixed.overridden', $map, 'Method-level override must replace, not duplicate');
// Other methods on the same class still use the default suffix
$this->assertArrayHasKey('class-prefixed.alpha', $map);
}
// ─────────────────────────────────────────────────────────────────
// resolve(), clear(), setSearchPaths()
// ─────────────────────────────────────────────────────────────────
/** @test */
public function resolve_returns_null_for_unknown_events()
{
$this->assertNull(EventRegistry::resolve('this.does-not-exist'));
}
/** @test */
public function resolve_returns_target_for_known_events()
{
$target = EventRegistry::resolve('plain.alpha');
$this->assertNotNull($target);
$this->assertSame(self::FIXTURES_NS . '\\PlainController', $target['class']);
$this->assertSame('alpha', $target['method']);
$this->assertFalse($target['needAuth']);
}
/** @test */
public function clear_invalidates_the_cache()
{
// Build the map once to populate the cache
EventRegistry::map();
// Re-point search paths to a non-existent directory and clear
EventRegistry::clear();
EventRegistry::setSearchPaths(['/nonexistent/path/that/does/not/exist' => 'App\\']);
// Map should now be empty (no fixtures discoverable from the bogus path)
$this->assertSame([], EventRegistry::map());
}
/** @test */
public function setSearchPaths_supports_explicit_path_to_namespace_map()
{
EventRegistry::clear();
EventRegistry::setSearchPaths([
__DIR__ . '/Fixtures/Controllers' => self::FIXTURES_NS,
]);
$this->assertNotEmpty(EventRegistry::map());
}
/** @test */
public function the_attribute_constructor_defaults_are_all_null_or_false()
{
$attr = new Websocket();
$this->assertNull($attr->event);
$this->assertNull($attr->prefix);
$this->assertNull($attr->suffix);
$this->assertFalse($attr->needAuth);
}
/** @test */
public function the_attribute_accepts_named_arguments()
{
$attr = new Websocket(event: 'a.b', prefix: 'a', suffix: 'b', needAuth: true);
$this->assertSame('a.b', $attr->event);
$this->assertSame('a', $attr->prefix);
$this->assertSame('b', $attr->suffix);
$this->assertTrue($attr->needAuth);
}
}