The package separates fast deterministic tests from checks that need a real Redis process or browser.
Install development dependencies, then run:
composer validate
composer analyze
composer test
composer test-browserOr run the combined script:
composer ciThe combined command requires Node.js 18 or newer for the framework-independent
browser client test. npm test is an equivalent direct command.
PHPUnit boots CodeIgniter's official test environment. The test suite should remain independent of an application checkout.
Unit tests cover:
- event creation, IDs, names, and JSON serialization;
- broker envelope versioning and invalid payloads;
- logical channel syntax and helper constructors;
- channel request limits and authorization;
- SSE frame encoding, multiline data, comments, and retry values;
- heartbeat and maximum-lifetime decisions;
- native response feature detection and legacy response output;
- Redis RESP parsing and command failures;
- browser-client status, listener, and parsing behavior.
Use fixed clocks and event ID generators where deterministic output matters.
Every broker implementation should satisfy the same public behavior:
Publisher contract
Subscriber contract
Pattern subscription contract, when supported
InMemoryBroker is useful for one-process unit tests:
use Maniaba\CodeIgniterSse\Broker\InMemory\InMemoryBroker;
$broker = new InMemoryBroker();It is not a Redis substitute for an end-to-end HTTP test. Separate PHP requests do not share its memory.
NullBroker is useful when a test must assert that publishing is harmless but
does not need delivery.
Run the repository's isolated Redis service:
docker compose up -d redisIt exposes Redis on host port 16379. Enable the live integration test:
SSE_REDIS_INTEGRATION=1 composer testRun the Mercure publisher/subscriber integration test against the development Hub:
docker compose up -d mercure
SSE_MERCURE_INTEGRATION=1 composer test -- --group integrationThe test issues a private topic JWT, opens a real Hub subscription, publishes
through MercurePublisher, and verifies the versioned event envelope received
over SSE.
For an externally managed test Redis, override the host and port:
SSE_REDIS_INTEGRATION=1 \
SSE_REDIS_HOST=127.0.0.1 \
SSE_REDIS_PORT=16379 \
composer testThe live test also requires pcntl_fork() and Unix stream socket pairs; it is
skipped when those functions are unavailable.
The bundled integration test uses an isolated integration:sse: channel
prefix; do not run multiple copies of it concurrently against the same Redis
instance. Custom parallel suites should generate a unique prefix per process.
Redis Pub/Sub is not isolated by numbered Redis databases. Tests must never
issue a broad Redis flush against a shared environment.
Integration scenarios should include:
- publisher sends and subscriber receives the same versioned event;
- two subscribers receive one broadcast;
- publisher and subscriber use distinct connections;
- ACL authentication and database selection;
- read timeout returns control for heartbeat processing;
- disconnect stops subscription;
- reconnect restores a subscription;
- overlapping patterns do not surprise the application contract;
- malformed RESP and Redis error frames fail predictably.
From a host CodeIgniter application, run the operational check after setting
the matching channelPrefix and redis config array values:
php spark sse:health-checkThe SSE_REDIS_HOST and SSE_REDIS_PORT variables above belong only to this
repository's integration test and are not read by the Spark command.
Feature tests should verify:
GET /sseroute discovery;- required
Accept: text/event-streamfor direct streams and JSON Mercure authorization; - missing, invalid, duplicate, and excessive channels;
- default
public.*access; - rejection of unauthorized private channels;
- CORS allowlist and credentials;
- connected event, retry field, event frames, and heartbeat comments;
- finite stream lifetime;
- session lock release in the legacy response;
- native and compatibility response selection.
Use recording implementations of SubscriberInterface and
SseOutputInterface so feature tests do not block.
SseClient accepts eventSourceFactory, and MercureSseAdapter accepts
fetchFactory, so tests can supply small deterministic fakes:
import {
RedisSseAdapter,
SseClient,
} from '@maniaba/codeigniter4-sse-browser';
const source = new FakeEventSource();
const live = new SseClient({
endpoint: 'https://example.test/sse',
adapter: new RedisSseAdapter(),
channels: ['public.test'],
eventSourceFactory: (url, options) => {
expect(url).toContain('channels=public.test');
expect(options.withCredentials).toBe(true);
return source;
},
});Test at least:
- query-string construction;
- named and global dispatch;
- handler removal;
- valid and invalid JSON;
connecting → open → reconnecting → open;- manual close;
- unsupported EventSource fallback;
- fallback errors not breaking other handlers.
A browser E2E scenario should:
open authenticated page
→ establish EventSource
→ publish from backend
→ verify DOM update
→ terminate the stream
→ observe native reconnect
→ publish another event
→ verify the second update
Also verify that a different authenticated user cannot subscribe to the first user's channel.
Use curl -N so output is not buffered:
curl -N \
-H 'Accept: text/event-stream' \
'http://localhost:8080/sse?channels=public.test'Expected initial fields include the reconnect hint and, when enabled, the
sse.connected event. Publish another event and verify that it appears before
the HTTP response ends.